From 97c86a2546d066efeba0e908b63a89ad25bbd28f Mon Sep 17 00:00:00 2001 From: Conor Schaefer Date: Fri, 22 Mar 2024 11:37:09 -0700 Subject: [PATCH 001/168] ci: fix readinessprobe on postgres container Follow-up to #4074, after additional migration testing of indexer nodes. --- .../charts/penumbra-node/templates/statefulset.yaml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/deployments/charts/penumbra-node/templates/statefulset.yaml b/deployments/charts/penumbra-node/templates/statefulset.yaml index af74cd5872..01432ae7e2 100644 --- a/deployments/charts/penumbra-node/templates/statefulset.yaml +++ b/deployments/charts/penumbra-node/templates/statefulset.yaml @@ -104,6 +104,8 @@ spec: - key: "tls.key" path: "server.key" {{ end }} + {{- if .Values.maintenanceMode }} + {{- else }} initContainers: - name: pd-init securityContext: @@ -171,6 +173,7 @@ spec: - name: db-certificates mountPath: /opt/postgres-certificates {{- end }} + {{- end }} containers: - name: pd @@ -262,7 +265,7 @@ spec: command: - sleep - infinity - {{- end }} + {{- else }} {{- if .Values.postgres.certificateSecretName }} args: - -c @@ -272,6 +275,7 @@ spec: - -c - ssl_key_file=/var/lib/postgresql/certs/server.key {{- end }} + {{- end }} ports: - name: postgres containerPort: 5432 @@ -305,12 +309,14 @@ spec: valueFrom: fieldRef: fieldPath: metadata.labels['apps.kubernetes.io/pod-index'] - + {{- if .Values.maintenanceMode }} + {{- else }} readinessProbe: tcpSocket: port: 5432 timeoutSeconds: 10 initialDelaySeconds: 10 + {{- end }} resources: {{- toYaml .Values.postgres.resources | nindent 12 }} volumeMounts: From 81a9b92128779e5571efccdeddaba875cbb38089 Mon Sep 17 00:00:00 2001 From: Conor Schaefer Date: Fri, 22 Mar 2024 14:37:32 -0700 Subject: [PATCH 002/168] ci: wait for all pods ready post-deploy Slightly smarter CI logic, which will block until all pods are marked Ready post-deployment. Due to an oversight, the "part-of" label wasn't applied to the fullnode pods, so the deploy script exited after the validators were running, but before the fullnodes were finished setting up. That was fine, until #3336, which tacked on a subsequent deploy step that assumes the RPC is ready to rock. Also updates the statefulsets to deploy the child pods in parallel, rather than serially, which shaves a few minutes off setup/teardown. Only really affects preview env, which has frequent deploy churn. --- deployments/charts/penumbra-network/templates/statefulset.yaml | 1 + deployments/charts/penumbra-node/templates/statefulset.yaml | 2 ++ deployments/ci.sh | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/deployments/charts/penumbra-network/templates/statefulset.yaml b/deployments/charts/penumbra-network/templates/statefulset.yaml index 9325bd4e48..4a230fd732 100644 --- a/deployments/charts/penumbra-network/templates/statefulset.yaml +++ b/deployments/charts/penumbra-network/templates/statefulset.yaml @@ -14,6 +14,7 @@ metadata: app.kubernetes.io/part-of: {{ include "penumbra-network.part_of" $ }} {{- include "penumbra-network.labels" $ | nindent 4 }} spec: + podManagementPolicy: Parallel replicas: {{ $count }} volumeClaimTemplates: - metadata: diff --git a/deployments/charts/penumbra-node/templates/statefulset.yaml b/deployments/charts/penumbra-node/templates/statefulset.yaml index 01432ae7e2..5472743bc7 100644 --- a/deployments/charts/penumbra-node/templates/statefulset.yaml +++ b/deployments/charts/penumbra-node/templates/statefulset.yaml @@ -11,6 +11,7 @@ metadata: "app.kubernetes.io/part-of": {{ .Values.part_of }} {{- end }} spec: + podManagementPolicy: Parallel replicas: {{ .Values.nodes | len | int }} volumeClaimTemplates: - metadata: @@ -48,6 +49,7 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} labels: + {{- include "penumbra-node.labels" . | nindent 8 }} {{- include "penumbra-node.selectorLabels" . | nindent 8 }} spec: # Force the pods to different k8s nodes, so that egress ip is unique per CometBFT node. diff --git a/deployments/ci.sh b/deployments/ci.sh index 9ff6f50557..9d46cde767 100755 --- a/deployments/ci.sh +++ b/deployments/ci.sh @@ -54,9 +54,10 @@ function helm_install() { } function wait_for_pods_to_be_running() { - echo "Waiting for pods to be running..." + echo "Waiting for pods to be running ($(date))..." kubectl wait --for=condition=ready pods --timeout=5m \ -l app.kubernetes.io/part-of="$HELM_RELEASE" + echo "Done waiting for pods to be running ($(date))" } # Deploy a fresh testnet, destroying all prior chain state with new genesis. From 77665ba0d969a36243bcf8bdd80ab03ad31bbd76 Mon Sep 17 00:00:00 2001 From: Conor Schaefer Date: Fri, 22 Mar 2024 13:47:00 -0700 Subject: [PATCH 003/168] chore: bump chain id to deimos-6 Refs #4087. --- testnets/070-deimos-6/allocations.csv | 409 ++++++++++++++++++++++++++ testnets/070-deimos-6/validators.json | 34 +++ 2 files changed, 443 insertions(+) create mode 100644 testnets/070-deimos-6/allocations.csv create mode 100644 testnets/070-deimos-6/validators.json diff --git a/testnets/070-deimos-6/allocations.csv b/testnets/070-deimos-6/allocations.csv new file mode 100644 index 0000000000..ca26d74b6c --- /dev/null +++ b/testnets/070-deimos-6/allocations.csv @@ -0,0 +1,409 @@ +amount,denom,address +1_000_000__000_000,upenumbra,penumbra1tj9s0dh8ymphnw2qgrva57wpcmpr38e9a8w05t5c8dyynr7dt58hgnare7tunqyuryc7yag084rqxrsrj9yx8uk9yhs8lmg64hl993wn3rphlvan3paxdd6suyhwnpkd6cwzes +20_000,gm,penumbra1tj9s0dh8ymphnw2qgrva57wpcmpr38e9a8w05t5c8dyynr7dt58hgnare7tunqyuryc7yag084rqxrsrj9yx8uk9yhs8lmg64hl993wn3rphlvan3paxdd6suyhwnpkd6cwzes +20_000,gn,penumbra1tj9s0dh8ymphnw2qgrva57wpcmpr38e9a8w05t5c8dyynr7dt58hgnare7tunqyuryc7yag084rqxrsrj9yx8uk9yhs8lmg64hl993wn3rphlvan3paxdd6suyhwnpkd6cwzes +10_000,pizza,penumbra1tj9s0dh8ymphnw2qgrva57wpcmpr38e9a8w05t5c8dyynr7dt58hgnare7tunqyuryc7yag084rqxrsrj9yx8uk9yhs8lmg64hl993wn3rphlvan3paxdd6suyhwnpkd6cwzes +100,cube,penumbra1tj9s0dh8ymphnw2qgrva57wpcmpr38e9a8w05t5c8dyynr7dt58hgnare7tunqyuryc7yag084rqxrsrj9yx8uk9yhs8lmg64hl993wn3rphlvan3paxdd6suyhwnpkd6cwzes +500_000,test_usd,penumbra1tj9s0dh8ymphnw2qgrva57wpcmpr38e9a8w05t5c8dyynr7dt58hgnare7tunqyuryc7yag084rqxrsrj9yx8uk9yhs8lmg64hl993wn3rphlvan3paxdd6suyhwnpkd6cwzes +1_000_000__000_000,upenumbra,penumbra12ts5g469dpjdcn2wmdkz25nng6snxkuxumfnmygtrp2fm6776zjw7r35y2j7s07h8368a4kr3c904w8cjgpm6v3ysaj6gh930z7fwlqq9kvjewsy9fgamzxyj874kgu5uvcshg +20_000,gm,penumbra12ts5g469dpjdcn2wmdkz25nng6snxkuxumfnmygtrp2fm6776zjw7r35y2j7s07h8368a4kr3c904w8cjgpm6v3ysaj6gh930z7fwlqq9kvjewsy9fgamzxyj874kgu5uvcshg +20_000,gn,penumbra12ts5g469dpjdcn2wmdkz25nng6snxkuxumfnmygtrp2fm6776zjw7r35y2j7s07h8368a4kr3c904w8cjgpm6v3ysaj6gh930z7fwlqq9kvjewsy9fgamzxyj874kgu5uvcshg +10_000,pizza,penumbra12ts5g469dpjdcn2wmdkz25nng6snxkuxumfnmygtrp2fm6776zjw7r35y2j7s07h8368a4kr3c904w8cjgpm6v3ysaj6gh930z7fwlqq9kvjewsy9fgamzxyj874kgu5uvcshg +100,cube,penumbra12ts5g469dpjdcn2wmdkz25nng6snxkuxumfnmygtrp2fm6776zjw7r35y2j7s07h8368a4kr3c904w8cjgpm6v3ysaj6gh930z7fwlqq9kvjewsy9fgamzxyj874kgu5uvcshg +500_000,test_usd,penumbra12ts5g469dpjdcn2wmdkz25nng6snxkuxumfnmygtrp2fm6776zjw7r35y2j7s07h8368a4kr3c904w8cjgpm6v3ysaj6gh930z7fwlqq9kvjewsy9fgamzxyj874kgu5uvcshg +1_000_000__000_000,upenumbra,penumbra19zz058ttl8vhsypztc0gyl9yfs7jcn3906kgd3pzeh944klh8vf2ttx7qvscxwtuecw92cy6n55ttjn482q7ufpzwj5yem9xcvecrd2zc6vgctxzc3k7mnpg0lk8vved00e3g0 +20_000,gm,penumbra19zz058ttl8vhsypztc0gyl9yfs7jcn3906kgd3pzeh944klh8vf2ttx7qvscxwtuecw92cy6n55ttjn482q7ufpzwj5yem9xcvecrd2zc6vgctxzc3k7mnpg0lk8vved00e3g0 +20_000,gn,penumbra19zz058ttl8vhsypztc0gyl9yfs7jcn3906kgd3pzeh944klh8vf2ttx7qvscxwtuecw92cy6n55ttjn482q7ufpzwj5yem9xcvecrd2zc6vgctxzc3k7mnpg0lk8vved00e3g0 +10_000,pizza,penumbra19zz058ttl8vhsypztc0gyl9yfs7jcn3906kgd3pzeh944klh8vf2ttx7qvscxwtuecw92cy6n55ttjn482q7ufpzwj5yem9xcvecrd2zc6vgctxzc3k7mnpg0lk8vved00e3g0 +100,cube,penumbra19zz058ttl8vhsypztc0gyl9yfs7jcn3906kgd3pzeh944klh8vf2ttx7qvscxwtuecw92cy6n55ttjn482q7ufpzwj5yem9xcvecrd2zc6vgctxzc3k7mnpg0lk8vved00e3g0 +500_000,test_usd,penumbra19zz058ttl8vhsypztc0gyl9yfs7jcn3906kgd3pzeh944klh8vf2ttx7qvscxwtuecw92cy6n55ttjn482q7ufpzwj5yem9xcvecrd2zc6vgctxzc3k7mnpg0lk8vved00e3g0 +1_000_000__000_000,upenumbra,penumbra1hckq024z3fd6wl29kk4rwfnykgyxyawpuw8zc04npc7s40hey0xghltdcsk7q5k0cq77qjtg8gt0cnvqff94s8j5tvx94ssyjyr35c9rqx08lkwxfqgr6dt3vu99wg8dg0c7jz +20_000,gm,penumbra1hckq024z3fd6wl29kk4rwfnykgyxyawpuw8zc04npc7s40hey0xghltdcsk7q5k0cq77qjtg8gt0cnvqff94s8j5tvx94ssyjyr35c9rqx08lkwxfqgr6dt3vu99wg8dg0c7jz +20_000,gn,penumbra1hckq024z3fd6wl29kk4rwfnykgyxyawpuw8zc04npc7s40hey0xghltdcsk7q5k0cq77qjtg8gt0cnvqff94s8j5tvx94ssyjyr35c9rqx08lkwxfqgr6dt3vu99wg8dg0c7jz +10_000,pizza,penumbra1hckq024z3fd6wl29kk4rwfnykgyxyawpuw8zc04npc7s40hey0xghltdcsk7q5k0cq77qjtg8gt0cnvqff94s8j5tvx94ssyjyr35c9rqx08lkwxfqgr6dt3vu99wg8dg0c7jz +100,cube,penumbra1hckq024z3fd6wl29kk4rwfnykgyxyawpuw8zc04npc7s40hey0xghltdcsk7q5k0cq77qjtg8gt0cnvqff94s8j5tvx94ssyjyr35c9rqx08lkwxfqgr6dt3vu99wg8dg0c7jz +500_000,test_usd,penumbra1hckq024z3fd6wl29kk4rwfnykgyxyawpuw8zc04npc7s40hey0xghltdcsk7q5k0cq77qjtg8gt0cnvqff94s8j5tvx94ssyjyr35c9rqx08lkwxfqgr6dt3vu99wg8dg0c7jz +1_000_000__000_000,upenumbra,penumbra1djxk3jzq5d3ga63vut39gdr9lh57yp34pu79jnjmjna3mmxuqaf476e7ar9je0hfn35cpuu4s7l9vae4gfd4773zepygwed5pfgdad26pvzd6qqaur4czes0cl5ku7jjvhx0e0 +20_000,gm,penumbra1djxk3jzq5d3ga63vut39gdr9lh57yp34pu79jnjmjna3mmxuqaf476e7ar9je0hfn35cpuu4s7l9vae4gfd4773zepygwed5pfgdad26pvzd6qqaur4czes0cl5ku7jjvhx0e0 +20_000,gn,penumbra1djxk3jzq5d3ga63vut39gdr9lh57yp34pu79jnjmjna3mmxuqaf476e7ar9je0hfn35cpuu4s7l9vae4gfd4773zepygwed5pfgdad26pvzd6qqaur4czes0cl5ku7jjvhx0e0 +10_000,pizza,penumbra1djxk3jzq5d3ga63vut39gdr9lh57yp34pu79jnjmjna3mmxuqaf476e7ar9je0hfn35cpuu4s7l9vae4gfd4773zepygwed5pfgdad26pvzd6qqaur4czes0cl5ku7jjvhx0e0 +100,cube,penumbra1djxk3jzq5d3ga63vut39gdr9lh57yp34pu79jnjmjna3mmxuqaf476e7ar9je0hfn35cpuu4s7l9vae4gfd4773zepygwed5pfgdad26pvzd6qqaur4czes0cl5ku7jjvhx0e0 +500_000,test_usd,penumbra1djxk3jzq5d3ga63vut39gdr9lh57yp34pu79jnjmjna3mmxuqaf476e7ar9je0hfn35cpuu4s7l9vae4gfd4773zepygwed5pfgdad26pvzd6qqaur4czes0cl5ku7jjvhx0e0 +1_000_000__000_000,upenumbra,penumbra1f5qra6dk8pmp0m2s42sq4qsxthjp6mmcpj3rjwrn74getpmwhrgzhe545fhz6eew2gdcpn2ee0j7fdm4whxl8ux6p5jz6g5fz72u4pzd4z26jnxeasvsvwapx2lvhxw8uy7dvw +20_000,gm,penumbra1f5qra6dk8pmp0m2s42sq4qsxthjp6mmcpj3rjwrn74getpmwhrgzhe545fhz6eew2gdcpn2ee0j7fdm4whxl8ux6p5jz6g5fz72u4pzd4z26jnxeasvsvwapx2lvhxw8uy7dvw +20_000,gn,penumbra1f5qra6dk8pmp0m2s42sq4qsxthjp6mmcpj3rjwrn74getpmwhrgzhe545fhz6eew2gdcpn2ee0j7fdm4whxl8ux6p5jz6g5fz72u4pzd4z26jnxeasvsvwapx2lvhxw8uy7dvw +10_000,pizza,penumbra1f5qra6dk8pmp0m2s42sq4qsxthjp6mmcpj3rjwrn74getpmwhrgzhe545fhz6eew2gdcpn2ee0j7fdm4whxl8ux6p5jz6g5fz72u4pzd4z26jnxeasvsvwapx2lvhxw8uy7dvw +100,cube,penumbra1f5qra6dk8pmp0m2s42sq4qsxthjp6mmcpj3rjwrn74getpmwhrgzhe545fhz6eew2gdcpn2ee0j7fdm4whxl8ux6p5jz6g5fz72u4pzd4z26jnxeasvsvwapx2lvhxw8uy7dvw +500_000,test_usd,penumbra1f5qra6dk8pmp0m2s42sq4qsxthjp6mmcpj3rjwrn74getpmwhrgzhe545fhz6eew2gdcpn2ee0j7fdm4whxl8ux6p5jz6g5fz72u4pzd4z26jnxeasvsvwapx2lvhxw8uy7dvw +1_000_000__000_000,upenumbra,penumbra18dms48wvudc7qljl6zjq48hyuvxxvhm2se2zltpjndpnn5c092mwych7uvs0xztpc7q0e2j7s89nyfxyj2pmu50zafvhwq5x4prm49ax6c7txmvjjdkgu9c8u0nhyqgrsmfn2c +20_000,gm,penumbra18dms48wvudc7qljl6zjq48hyuvxxvhm2se2zltpjndpnn5c092mwych7uvs0xztpc7q0e2j7s89nyfxyj2pmu50zafvhwq5x4prm49ax6c7txmvjjdkgu9c8u0nhyqgrsmfn2c +20_000,gn,penumbra18dms48wvudc7qljl6zjq48hyuvxxvhm2se2zltpjndpnn5c092mwych7uvs0xztpc7q0e2j7s89nyfxyj2pmu50zafvhwq5x4prm49ax6c7txmvjjdkgu9c8u0nhyqgrsmfn2c +10_000,pizza,penumbra18dms48wvudc7qljl6zjq48hyuvxxvhm2se2zltpjndpnn5c092mwych7uvs0xztpc7q0e2j7s89nyfxyj2pmu50zafvhwq5x4prm49ax6c7txmvjjdkgu9c8u0nhyqgrsmfn2c +100,cube,penumbra18dms48wvudc7qljl6zjq48hyuvxxvhm2se2zltpjndpnn5c092mwych7uvs0xztpc7q0e2j7s89nyfxyj2pmu50zafvhwq5x4prm49ax6c7txmvjjdkgu9c8u0nhyqgrsmfn2c +500_000,test_usd,penumbra18dms48wvudc7qljl6zjq48hyuvxxvhm2se2zltpjndpnn5c092mwych7uvs0xztpc7q0e2j7s89nyfxyj2pmu50zafvhwq5x4prm49ax6c7txmvjjdkgu9c8u0nhyqgrsmfn2c +1_000_000__000_000,upenumbra,penumbra169w7gexlfa9m7gsa3vmwyfpu92j0yxawmlkt06g3qg5ta29czwzepzqp0d0nl324jgcjrc6fy0kmqhjlureeyjulejug8e2h6zj0kqf0vapwszahqm8swg634extm3837kl59v +20_000,gm,penumbra169w7gexlfa9m7gsa3vmwyfpu92j0yxawmlkt06g3qg5ta29czwzepzqp0d0nl324jgcjrc6fy0kmqhjlureeyjulejug8e2h6zj0kqf0vapwszahqm8swg634extm3837kl59v +20_000,gn,penumbra169w7gexlfa9m7gsa3vmwyfpu92j0yxawmlkt06g3qg5ta29czwzepzqp0d0nl324jgcjrc6fy0kmqhjlureeyjulejug8e2h6zj0kqf0vapwszahqm8swg634extm3837kl59v +10_000,pizza,penumbra169w7gexlfa9m7gsa3vmwyfpu92j0yxawmlkt06g3qg5ta29czwzepzqp0d0nl324jgcjrc6fy0kmqhjlureeyjulejug8e2h6zj0kqf0vapwszahqm8swg634extm3837kl59v +100,cube,penumbra169w7gexlfa9m7gsa3vmwyfpu92j0yxawmlkt06g3qg5ta29czwzepzqp0d0nl324jgcjrc6fy0kmqhjlureeyjulejug8e2h6zj0kqf0vapwszahqm8swg634extm3837kl59v +500_000,test_usd,penumbra169w7gexlfa9m7gsa3vmwyfpu92j0yxawmlkt06g3qg5ta29czwzepzqp0d0nl324jgcjrc6fy0kmqhjlureeyjulejug8e2h6zj0kqf0vapwszahqm8swg634extm3837kl59v +1_000_000__000_000,upenumbra,penumbra10as9rd2ny8w6v70sx4cetrph9twy5vu4ystjz6ae0uuutjamfztl2649vdufmya02a6r0907ywuxkhz3uve6thaf5et2jhnx97k0h0t07ad9pdhawls7wzklrktefzwwvrxtsn +20_000,gm,penumbra10as9rd2ny8w6v70sx4cetrph9twy5vu4ystjz6ae0uuutjamfztl2649vdufmya02a6r0907ywuxkhz3uve6thaf5et2jhnx97k0h0t07ad9pdhawls7wzklrktefzwwvrxtsn +20_000,gn,penumbra10as9rd2ny8w6v70sx4cetrph9twy5vu4ystjz6ae0uuutjamfztl2649vdufmya02a6r0907ywuxkhz3uve6thaf5et2jhnx97k0h0t07ad9pdhawls7wzklrktefzwwvrxtsn +10_000,pizza,penumbra10as9rd2ny8w6v70sx4cetrph9twy5vu4ystjz6ae0uuutjamfztl2649vdufmya02a6r0907ywuxkhz3uve6thaf5et2jhnx97k0h0t07ad9pdhawls7wzklrktefzwwvrxtsn +100,cube,penumbra10as9rd2ny8w6v70sx4cetrph9twy5vu4ystjz6ae0uuutjamfztl2649vdufmya02a6r0907ywuxkhz3uve6thaf5et2jhnx97k0h0t07ad9pdhawls7wzklrktefzwwvrxtsn +500_000,test_usd,penumbra10as9rd2ny8w6v70sx4cetrph9twy5vu4ystjz6ae0uuutjamfztl2649vdufmya02a6r0907ywuxkhz3uve6thaf5et2jhnx97k0h0t07ad9pdhawls7wzklrktefzwwvrxtsn +1_000_000__000_000,upenumbra,penumbra1ks9t4vrp9alvk9yfapyu75hjf7p3wva76t0dz9a902zlp8vuyn0q9j2a72pu9j0uxmg356xstqpmafkj28ktx37l4lzq25nmgdeay5ls00yky2pgladnr5z7u3ftav7lw9vnre +20_000,gm,penumbra1ks9t4vrp9alvk9yfapyu75hjf7p3wva76t0dz9a902zlp8vuyn0q9j2a72pu9j0uxmg356xstqpmafkj28ktx37l4lzq25nmgdeay5ls00yky2pgladnr5z7u3ftav7lw9vnre +20_000,gn,penumbra1ks9t4vrp9alvk9yfapyu75hjf7p3wva76t0dz9a902zlp8vuyn0q9j2a72pu9j0uxmg356xstqpmafkj28ktx37l4lzq25nmgdeay5ls00yky2pgladnr5z7u3ftav7lw9vnre +10_000,pizza,penumbra1ks9t4vrp9alvk9yfapyu75hjf7p3wva76t0dz9a902zlp8vuyn0q9j2a72pu9j0uxmg356xstqpmafkj28ktx37l4lzq25nmgdeay5ls00yky2pgladnr5z7u3ftav7lw9vnre +100,cube,penumbra1ks9t4vrp9alvk9yfapyu75hjf7p3wva76t0dz9a902zlp8vuyn0q9j2a72pu9j0uxmg356xstqpmafkj28ktx37l4lzq25nmgdeay5ls00yky2pgladnr5z7u3ftav7lw9vnre +500_000,test_usd,penumbra1ks9t4vrp9alvk9yfapyu75hjf7p3wva76t0dz9a902zlp8vuyn0q9j2a72pu9j0uxmg356xstqpmafkj28ktx37l4lzq25nmgdeay5ls00yky2pgladnr5z7u3ftav7lw9vnre +1_000_000__000_000,upenumbra,penumbra183uj3sh6d22j6mguu3vlpfcp87lh93jfzn3ucehfk2j4ek07jnwelznq2k8jk200gwrnyyvttexay2u638l7s6dhnh7vec3wscyefslswnhwd44ywqfxzxd4wdt8eqxy8st7k0 +20_000,gm,penumbra183uj3sh6d22j6mguu3vlpfcp87lh93jfzn3ucehfk2j4ek07jnwelznq2k8jk200gwrnyyvttexay2u638l7s6dhnh7vec3wscyefslswnhwd44ywqfxzxd4wdt8eqxy8st7k0 +20_000,gn,penumbra183uj3sh6d22j6mguu3vlpfcp87lh93jfzn3ucehfk2j4ek07jnwelznq2k8jk200gwrnyyvttexay2u638l7s6dhnh7vec3wscyefslswnhwd44ywqfxzxd4wdt8eqxy8st7k0 +10_000,pizza,penumbra183uj3sh6d22j6mguu3vlpfcp87lh93jfzn3ucehfk2j4ek07jnwelznq2k8jk200gwrnyyvttexay2u638l7s6dhnh7vec3wscyefslswnhwd44ywqfxzxd4wdt8eqxy8st7k0 +100,cube,penumbra183uj3sh6d22j6mguu3vlpfcp87lh93jfzn3ucehfk2j4ek07jnwelznq2k8jk200gwrnyyvttexay2u638l7s6dhnh7vec3wscyefslswnhwd44ywqfxzxd4wdt8eqxy8st7k0 +500_000,test_usd,penumbra183uj3sh6d22j6mguu3vlpfcp87lh93jfzn3ucehfk2j4ek07jnwelznq2k8jk200gwrnyyvttexay2u638l7s6dhnh7vec3wscyefslswnhwd44ywqfxzxd4wdt8eqxy8st7k0 +1_000_000__000_000,upenumbra,penumbra1u8eg9v2dlge3yd7gjkdmnzj3quvgwy204ueryl0uzxpjjpan4fwkjwd8mlrkpfffe6vuweeq0x7wn3rhljj7qaedye4kuhpj9fmcu4zl0cn6up0syg60hrc70x9edf2gy0shft +20_000,gm,penumbra1u8eg9v2dlge3yd7gjkdmnzj3quvgwy204ueryl0uzxpjjpan4fwkjwd8mlrkpfffe6vuweeq0x7wn3rhljj7qaedye4kuhpj9fmcu4zl0cn6up0syg60hrc70x9edf2gy0shft +20_000,gn,penumbra1u8eg9v2dlge3yd7gjkdmnzj3quvgwy204ueryl0uzxpjjpan4fwkjwd8mlrkpfffe6vuweeq0x7wn3rhljj7qaedye4kuhpj9fmcu4zl0cn6up0syg60hrc70x9edf2gy0shft +10_000,pizza,penumbra1u8eg9v2dlge3yd7gjkdmnzj3quvgwy204ueryl0uzxpjjpan4fwkjwd8mlrkpfffe6vuweeq0x7wn3rhljj7qaedye4kuhpj9fmcu4zl0cn6up0syg60hrc70x9edf2gy0shft +100,cube,penumbra1u8eg9v2dlge3yd7gjkdmnzj3quvgwy204ueryl0uzxpjjpan4fwkjwd8mlrkpfffe6vuweeq0x7wn3rhljj7qaedye4kuhpj9fmcu4zl0cn6up0syg60hrc70x9edf2gy0shft +500_000,test_usd,penumbra1u8eg9v2dlge3yd7gjkdmnzj3quvgwy204ueryl0uzxpjjpan4fwkjwd8mlrkpfffe6vuweeq0x7wn3rhljj7qaedye4kuhpj9fmcu4zl0cn6up0syg60hrc70x9edf2gy0shft +1_000_000__000_000,upenumbra,penumbra1r70z64vqnmv28rttpv60hppd7gca9w5a3zwctx4c6yrtfd4mlvjrevkx3l2swdnw8vq7amylgfwaq2x97dlxmmh0flpxetmd8gjf0rpmg083ms94psmvpgy7hgdhwwn6nr92sc +20_000,gm,penumbra1r70z64vqnmv28rttpv60hppd7gca9w5a3zwctx4c6yrtfd4mlvjrevkx3l2swdnw8vq7amylgfwaq2x97dlxmmh0flpxetmd8gjf0rpmg083ms94psmvpgy7hgdhwwn6nr92sc +20_000,gn,penumbra1r70z64vqnmv28rttpv60hppd7gca9w5a3zwctx4c6yrtfd4mlvjrevkx3l2swdnw8vq7amylgfwaq2x97dlxmmh0flpxetmd8gjf0rpmg083ms94psmvpgy7hgdhwwn6nr92sc +10_000,pizza,penumbra1r70z64vqnmv28rttpv60hppd7gca9w5a3zwctx4c6yrtfd4mlvjrevkx3l2swdnw8vq7amylgfwaq2x97dlxmmh0flpxetmd8gjf0rpmg083ms94psmvpgy7hgdhwwn6nr92sc +100,cube,penumbra1r70z64vqnmv28rttpv60hppd7gca9w5a3zwctx4c6yrtfd4mlvjrevkx3l2swdnw8vq7amylgfwaq2x97dlxmmh0flpxetmd8gjf0rpmg083ms94psmvpgy7hgdhwwn6nr92sc +500_000,test_usd,penumbra1r70z64vqnmv28rttpv60hppd7gca9w5a3zwctx4c6yrtfd4mlvjrevkx3l2swdnw8vq7amylgfwaq2x97dlxmmh0flpxetmd8gjf0rpmg083ms94psmvpgy7hgdhwwn6nr92sc +1_000_000__000_000,upenumbra,penumbra18z69jxec5008krvlhw84459ecermc2x5r06flunlz6m45ftmphp2djp9cpxy4n4m2tykz00jjd3pgm5n3etpaq43ypt4xffcz8ag0ay3hutt7hdfevmjfft237j2x2vae99v3f +20_000,gm,penumbra18z69jxec5008krvlhw84459ecermc2x5r06flunlz6m45ftmphp2djp9cpxy4n4m2tykz00jjd3pgm5n3etpaq43ypt4xffcz8ag0ay3hutt7hdfevmjfft237j2x2vae99v3f +20_000,gn,penumbra18z69jxec5008krvlhw84459ecermc2x5r06flunlz6m45ftmphp2djp9cpxy4n4m2tykz00jjd3pgm5n3etpaq43ypt4xffcz8ag0ay3hutt7hdfevmjfft237j2x2vae99v3f +10_000,pizza,penumbra18z69jxec5008krvlhw84459ecermc2x5r06flunlz6m45ftmphp2djp9cpxy4n4m2tykz00jjd3pgm5n3etpaq43ypt4xffcz8ag0ay3hutt7hdfevmjfft237j2x2vae99v3f +100,cube,penumbra18z69jxec5008krvlhw84459ecermc2x5r06flunlz6m45ftmphp2djp9cpxy4n4m2tykz00jjd3pgm5n3etpaq43ypt4xffcz8ag0ay3hutt7hdfevmjfft237j2x2vae99v3f +500_000,test_usd,penumbra18z69jxec5008krvlhw84459ecermc2x5r06flunlz6m45ftmphp2djp9cpxy4n4m2tykz00jjd3pgm5n3etpaq43ypt4xffcz8ag0ay3hutt7hdfevmjfft237j2x2vae99v3f +1_000_000__000_000,upenumbra,penumbra1q87euedu4yw3f8c025zulgu6tgc8frmhak7ge6xwyauuptlp4z25mn92z6qpa9nhdp08rfs3lakxqpgpd58m8u02cjcrpec0xdt6t9zlgq7fkxmklvtpktykwn0rdlxeqyny2n +20_000,gm,penumbra1q87euedu4yw3f8c025zulgu6tgc8frmhak7ge6xwyauuptlp4z25mn92z6qpa9nhdp08rfs3lakxqpgpd58m8u02cjcrpec0xdt6t9zlgq7fkxmklvtpktykwn0rdlxeqyny2n +20_000,gn,penumbra1q87euedu4yw3f8c025zulgu6tgc8frmhak7ge6xwyauuptlp4z25mn92z6qpa9nhdp08rfs3lakxqpgpd58m8u02cjcrpec0xdt6t9zlgq7fkxmklvtpktykwn0rdlxeqyny2n +10_000,pizza,penumbra1q87euedu4yw3f8c025zulgu6tgc8frmhak7ge6xwyauuptlp4z25mn92z6qpa9nhdp08rfs3lakxqpgpd58m8u02cjcrpec0xdt6t9zlgq7fkxmklvtpktykwn0rdlxeqyny2n +100,cube,penumbra1q87euedu4yw3f8c025zulgu6tgc8frmhak7ge6xwyauuptlp4z25mn92z6qpa9nhdp08rfs3lakxqpgpd58m8u02cjcrpec0xdt6t9zlgq7fkxmklvtpktykwn0rdlxeqyny2n +500_000,test_usd,penumbra1q87euedu4yw3f8c025zulgu6tgc8frmhak7ge6xwyauuptlp4z25mn92z6qpa9nhdp08rfs3lakxqpgpd58m8u02cjcrpec0xdt6t9zlgq7fkxmklvtpktykwn0rdlxeqyny2n +1_000_000__000_000,upenumbra,penumbra1zn54t9n55da28vkytffkkrlfxcwutn4nlj05xwfw4jmgzr60jfqazvahzwz2ts9j0juru8r8ghwr47klmcvuhny6jfcunahs84z7fmgkk54xvfmvveqh4yjc69sg0jrf92r6kn +20_000,gm,penumbra1zn54t9n55da28vkytffkkrlfxcwutn4nlj05xwfw4jmgzr60jfqazvahzwz2ts9j0juru8r8ghwr47klmcvuhny6jfcunahs84z7fmgkk54xvfmvveqh4yjc69sg0jrf92r6kn +20_000,gn,penumbra1zn54t9n55da28vkytffkkrlfxcwutn4nlj05xwfw4jmgzr60jfqazvahzwz2ts9j0juru8r8ghwr47klmcvuhny6jfcunahs84z7fmgkk54xvfmvveqh4yjc69sg0jrf92r6kn +10_000,pizza,penumbra1zn54t9n55da28vkytffkkrlfxcwutn4nlj05xwfw4jmgzr60jfqazvahzwz2ts9j0juru8r8ghwr47klmcvuhny6jfcunahs84z7fmgkk54xvfmvveqh4yjc69sg0jrf92r6kn +100,cube,penumbra1zn54t9n55da28vkytffkkrlfxcwutn4nlj05xwfw4jmgzr60jfqazvahzwz2ts9j0juru8r8ghwr47klmcvuhny6jfcunahs84z7fmgkk54xvfmvveqh4yjc69sg0jrf92r6kn +500_000,test_usd,penumbra1zn54t9n55da28vkytffkkrlfxcwutn4nlj05xwfw4jmgzr60jfqazvahzwz2ts9j0juru8r8ghwr47klmcvuhny6jfcunahs84z7fmgkk54xvfmvveqh4yjc69sg0jrf92r6kn +1_000_000__000_000,upenumbra,penumbra10kqp4qk220hc2flys45czh2vsc30jwl4mfy8styvjnz0zlhtw6gef74k0v2r7nqghlkuauxd9wafpz2nh8rjfqp747hc3hxakml793rjau6ljnm46vlmnpmczt4jkq8xcdakgg +20_000,gm,penumbra10kqp4qk220hc2flys45czh2vsc30jwl4mfy8styvjnz0zlhtw6gef74k0v2r7nqghlkuauxd9wafpz2nh8rjfqp747hc3hxakml793rjau6ljnm46vlmnpmczt4jkq8xcdakgg +20_000,gn,penumbra10kqp4qk220hc2flys45czh2vsc30jwl4mfy8styvjnz0zlhtw6gef74k0v2r7nqghlkuauxd9wafpz2nh8rjfqp747hc3hxakml793rjau6ljnm46vlmnpmczt4jkq8xcdakgg +10_000,pizza,penumbra10kqp4qk220hc2flys45czh2vsc30jwl4mfy8styvjnz0zlhtw6gef74k0v2r7nqghlkuauxd9wafpz2nh8rjfqp747hc3hxakml793rjau6ljnm46vlmnpmczt4jkq8xcdakgg +100,cube,penumbra10kqp4qk220hc2flys45czh2vsc30jwl4mfy8styvjnz0zlhtw6gef74k0v2r7nqghlkuauxd9wafpz2nh8rjfqp747hc3hxakml793rjau6ljnm46vlmnpmczt4jkq8xcdakgg +500_000,test_usd,penumbra10kqp4qk220hc2flys45czh2vsc30jwl4mfy8styvjnz0zlhtw6gef74k0v2r7nqghlkuauxd9wafpz2nh8rjfqp747hc3hxakml793rjau6ljnm46vlmnpmczt4jkq8xcdakgg +5_000_000__000_000,upenumbra,penumbra1xq2e9x7uhfzezwunvazdamlxepf4jr5htsuqnzlsahuayyqxjjwg9lk0aytwm6wfj3jy29rv2kdpen57903s8wxv3jmqwj6m6v5jgn6y2cypfd03rke652k8wmavxra7e9wkrg +50_000,gm,penumbra1xq2e9x7uhfzezwunvazdamlxepf4jr5htsuqnzlsahuayyqxjjwg9lk0aytwm6wfj3jy29rv2kdpen57903s8wxv3jmqwj6m6v5jgn6y2cypfd03rke652k8wmavxra7e9wkrg +50_000,gn,penumbra1xq2e9x7uhfzezwunvazdamlxepf4jr5htsuqnzlsahuayyqxjjwg9lk0aytwm6wfj3jy29rv2kdpen57903s8wxv3jmqwj6m6v5jgn6y2cypfd03rke652k8wmavxra7e9wkrg +25_000,pizza,penumbra1xq2e9x7uhfzezwunvazdamlxepf4jr5htsuqnzlsahuayyqxjjwg9lk0aytwm6wfj3jy29rv2kdpen57903s8wxv3jmqwj6m6v5jgn6y2cypfd03rke652k8wmavxra7e9wkrg +250,cube,penumbra1xq2e9x7uhfzezwunvazdamlxepf4jr5htsuqnzlsahuayyqxjjwg9lk0aytwm6wfj3jy29rv2kdpen57903s8wxv3jmqwj6m6v5jgn6y2cypfd03rke652k8wmavxra7e9wkrg +1_000_000,test_usd,penumbra1xq2e9x7uhfzezwunvazdamlxepf4jr5htsuqnzlsahuayyqxjjwg9lk0aytwm6wfj3jy29rv2kdpen57903s8wxv3jmqwj6m6v5jgn6y2cypfd03rke652k8wmavxra7e9wkrg +10_000,nala,penumbra1xq2e9x7uhfzezwunvazdamlxepf4jr5htsuqnzlsahuayyqxjjwg9lk0aytwm6wfj3jy29rv2kdpen57903s8wxv3jmqwj6m6v5jgn6y2cypfd03rke652k8wmavxra7e9wkrg +5_000_000__000_000,upenumbra,penumbra1mflr95lhj0vjr86uyffkmxg6hjamjs7rrcykwgnyt8lcz0a6xqzh0xz78hpnq3x70s8cnu4cnyqq2wgkt9nthcc3k8scf4wlraccdmkr6wyzsap88t0du2dlv5xclgkmwmwnct +50_000,gm,penumbra1mflr95lhj0vjr86uyffkmxg6hjamjs7rrcykwgnyt8lcz0a6xqzh0xz78hpnq3x70s8cnu4cnyqq2wgkt9nthcc3k8scf4wlraccdmkr6wyzsap88t0du2dlv5xclgkmwmwnct +50_000,gn,penumbra1mflr95lhj0vjr86uyffkmxg6hjamjs7rrcykwgnyt8lcz0a6xqzh0xz78hpnq3x70s8cnu4cnyqq2wgkt9nthcc3k8scf4wlraccdmkr6wyzsap88t0du2dlv5xclgkmwmwnct +25_000,pizza,penumbra1mflr95lhj0vjr86uyffkmxg6hjamjs7rrcykwgnyt8lcz0a6xqzh0xz78hpnq3x70s8cnu4cnyqq2wgkt9nthcc3k8scf4wlraccdmkr6wyzsap88t0du2dlv5xclgkmwmwnct +250,cube,penumbra1mflr95lhj0vjr86uyffkmxg6hjamjs7rrcykwgnyt8lcz0a6xqzh0xz78hpnq3x70s8cnu4cnyqq2wgkt9nthcc3k8scf4wlraccdmkr6wyzsap88t0du2dlv5xclgkmwmwnct +1_000_000,test_usd,penumbra1mflr95lhj0vjr86uyffkmxg6hjamjs7rrcykwgnyt8lcz0a6xqzh0xz78hpnq3x70s8cnu4cnyqq2wgkt9nthcc3k8scf4wlraccdmkr6wyzsap88t0du2dlv5xclgkmwmwnct +10_000,nala,penumbra1mflr95lhj0vjr86uyffkmxg6hjamjs7rrcykwgnyt8lcz0a6xqzh0xz78hpnq3x70s8cnu4cnyqq2wgkt9nthcc3k8scf4wlraccdmkr6wyzsap88t0du2dlv5xclgkmwmwnct +5_000_000__000_000,upenumbra,penumbra1jdgsqxnqqhqwjc6j3lhm4qnqz4gu02j87p4v0uj95ur9xdtym9acnj6p6ykqx89crmj0gw8r6fl4xh4la2yxyks9czl4lvqsxm9e5u86gdus0fh9hqxvg8aq47cw9fv8l2vh34 +50_000,gm,penumbra1jdgsqxnqqhqwjc6j3lhm4qnqz4gu02j87p4v0uj95ur9xdtym9acnj6p6ykqx89crmj0gw8r6fl4xh4la2yxyks9czl4lvqsxm9e5u86gdus0fh9hqxvg8aq47cw9fv8l2vh34 +50_000,gn,penumbra1jdgsqxnqqhqwjc6j3lhm4qnqz4gu02j87p4v0uj95ur9xdtym9acnj6p6ykqx89crmj0gw8r6fl4xh4la2yxyks9czl4lvqsxm9e5u86gdus0fh9hqxvg8aq47cw9fv8l2vh34 +25_000,pizza,penumbra1jdgsqxnqqhqwjc6j3lhm4qnqz4gu02j87p4v0uj95ur9xdtym9acnj6p6ykqx89crmj0gw8r6fl4xh4la2yxyks9czl4lvqsxm9e5u86gdus0fh9hqxvg8aq47cw9fv8l2vh34 +250,cube,penumbra1jdgsqxnqqhqwjc6j3lhm4qnqz4gu02j87p4v0uj95ur9xdtym9acnj6p6ykqx89crmj0gw8r6fl4xh4la2yxyks9czl4lvqsxm9e5u86gdus0fh9hqxvg8aq47cw9fv8l2vh34 +1_000_000,test_usd,penumbra1jdgsqxnqqhqwjc6j3lhm4qnqz4gu02j87p4v0uj95ur9xdtym9acnj6p6ykqx89crmj0gw8r6fl4xh4la2yxyks9czl4lvqsxm9e5u86gdus0fh9hqxvg8aq47cw9fv8l2vh34 +10_000,nala,penumbra1jdgsqxnqqhqwjc6j3lhm4qnqz4gu02j87p4v0uj95ur9xdtym9acnj6p6ykqx89crmj0gw8r6fl4xh4la2yxyks9czl4lvqsxm9e5u86gdus0fh9hqxvg8aq47cw9fv8l2vh34 +5_000_000__000_000,upenumbra,penumbra1k20526f5je6c3mftpuu7p7mn3tr3p84x4qgm2nla9czwae9awse6mqsvn8qgr83y3cnhh7wkwv0s29ycs9r4p6lr5eeee0emquun8v7qlt7lt8w7ck3ukl5ctnufhzwmj00utn +50_000,gm,penumbra1k20526f5je6c3mftpuu7p7mn3tr3p84x4qgm2nla9czwae9awse6mqsvn8qgr83y3cnhh7wkwv0s29ycs9r4p6lr5eeee0emquun8v7qlt7lt8w7ck3ukl5ctnufhzwmj00utn +50_000,gn,penumbra1k20526f5je6c3mftpuu7p7mn3tr3p84x4qgm2nla9czwae9awse6mqsvn8qgr83y3cnhh7wkwv0s29ycs9r4p6lr5eeee0emquun8v7qlt7lt8w7ck3ukl5ctnufhzwmj00utn +25_000,pizza,penumbra1k20526f5je6c3mftpuu7p7mn3tr3p84x4qgm2nla9czwae9awse6mqsvn8qgr83y3cnhh7wkwv0s29ycs9r4p6lr5eeee0emquun8v7qlt7lt8w7ck3ukl5ctnufhzwmj00utn +250,cube,penumbra1k20526f5je6c3mftpuu7p7mn3tr3p84x4qgm2nla9czwae9awse6mqsvn8qgr83y3cnhh7wkwv0s29ycs9r4p6lr5eeee0emquun8v7qlt7lt8w7ck3ukl5ctnufhzwmj00utn +1_000_000,test_usd,penumbra1k20526f5je6c3mftpuu7p7mn3tr3p84x4qgm2nla9czwae9awse6mqsvn8qgr83y3cnhh7wkwv0s29ycs9r4p6lr5eeee0emquun8v7qlt7lt8w7ck3ukl5ctnufhzwmj00utn +10_000,nala,penumbra1k20526f5je6c3mftpuu7p7mn3tr3p84x4qgm2nla9czwae9awse6mqsvn8qgr83y3cnhh7wkwv0s29ycs9r4p6lr5eeee0emquun8v7qlt7lt8w7ck3ukl5ctnufhzwmj00utn +5_000_000__000_000,upenumbra,penumbra1s03zdrdjy4uc68rwtp2mz94lgg0k8fuyma4pphk6v2a66dap2d8xfd0l8rmtnh8l2pxdxyyy6gn9rqyzuga36sjnd30w6t0prq82wxve0tlx3veme9qpjlq82uvh6eytujzefr +50_000,gm,penumbra1s03zdrdjy4uc68rwtp2mz94lgg0k8fuyma4pphk6v2a66dap2d8xfd0l8rmtnh8l2pxdxyyy6gn9rqyzuga36sjnd30w6t0prq82wxve0tlx3veme9qpjlq82uvh6eytujzefr +50_000,gn,penumbra1s03zdrdjy4uc68rwtp2mz94lgg0k8fuyma4pphk6v2a66dap2d8xfd0l8rmtnh8l2pxdxyyy6gn9rqyzuga36sjnd30w6t0prq82wxve0tlx3veme9qpjlq82uvh6eytujzefr +25_000,pizza,penumbra1s03zdrdjy4uc68rwtp2mz94lgg0k8fuyma4pphk6v2a66dap2d8xfd0l8rmtnh8l2pxdxyyy6gn9rqyzuga36sjnd30w6t0prq82wxve0tlx3veme9qpjlq82uvh6eytujzefr +250,cube,penumbra1s03zdrdjy4uc68rwtp2mz94lgg0k8fuyma4pphk6v2a66dap2d8xfd0l8rmtnh8l2pxdxyyy6gn9rqyzuga36sjnd30w6t0prq82wxve0tlx3veme9qpjlq82uvh6eytujzefr +1_000_000,test_usd,penumbra1s03zdrdjy4uc68rwtp2mz94lgg0k8fuyma4pphk6v2a66dap2d8xfd0l8rmtnh8l2pxdxyyy6gn9rqyzuga36sjnd30w6t0prq82wxve0tlx3veme9qpjlq82uvh6eytujzefr +10_000,nala,penumbra1s03zdrdjy4uc68rwtp2mz94lgg0k8fuyma4pphk6v2a66dap2d8xfd0l8rmtnh8l2pxdxyyy6gn9rqyzuga36sjnd30w6t0prq82wxve0tlx3veme9qpjlq82uvh6eytujzefr +5_000_000__000_000,upenumbra,penumbra1ydlrv3mu25v5sz4fy0729lktq2xfwdhe4sfpe8886zmh82qn8j49ysnjc7cp3j4jzyp55m0a8dtr38uetzx97wjvcce5a9w42n5r7k5xzsypp59k0wkay5mq030lgdhz9pnf5w +50_000,gm,penumbra1ydlrv3mu25v5sz4fy0729lktq2xfwdhe4sfpe8886zmh82qn8j49ysnjc7cp3j4jzyp55m0a8dtr38uetzx97wjvcce5a9w42n5r7k5xzsypp59k0wkay5mq030lgdhz9pnf5w +50_000,gn,penumbra1ydlrv3mu25v5sz4fy0729lktq2xfwdhe4sfpe8886zmh82qn8j49ysnjc7cp3j4jzyp55m0a8dtr38uetzx97wjvcce5a9w42n5r7k5xzsypp59k0wkay5mq030lgdhz9pnf5w +25_000,pizza,penumbra1ydlrv3mu25v5sz4fy0729lktq2xfwdhe4sfpe8886zmh82qn8j49ysnjc7cp3j4jzyp55m0a8dtr38uetzx97wjvcce5a9w42n5r7k5xzsypp59k0wkay5mq030lgdhz9pnf5w +250,cube,penumbra1ydlrv3mu25v5sz4fy0729lktq2xfwdhe4sfpe8886zmh82qn8j49ysnjc7cp3j4jzyp55m0a8dtr38uetzx97wjvcce5a9w42n5r7k5xzsypp59k0wkay5mq030lgdhz9pnf5w +1_000_000,test_usd,penumbra1ydlrv3mu25v5sz4fy0729lktq2xfwdhe4sfpe8886zmh82qn8j49ysnjc7cp3j4jzyp55m0a8dtr38uetzx97wjvcce5a9w42n5r7k5xzsypp59k0wkay5mq030lgdhz9pnf5w +10_000,nala,penumbra1ydlrv3mu25v5sz4fy0729lktq2xfwdhe4sfpe8886zmh82qn8j49ysnjc7cp3j4jzyp55m0a8dtr38uetzx97wjvcce5a9w42n5r7k5xzsypp59k0wkay5mq030lgdhz9pnf5w +5_000_000__000_000,upenumbra,penumbra1csu9gn9x9pt237s3jmc9sen6d0th582623e737s9hymqp55hwnfp3j3mhjcksktzqpx0qls6n4anc9nl82w7y5zpeztg4dxma6lmgw6dwj76v7drqn8tznl86jnrpvm3zfg3y4 +50_000,gm,penumbra1csu9gn9x9pt237s3jmc9sen6d0th582623e737s9hymqp55hwnfp3j3mhjcksktzqpx0qls6n4anc9nl82w7y5zpeztg4dxma6lmgw6dwj76v7drqn8tznl86jnrpvm3zfg3y4 +50_000,gn,penumbra1csu9gn9x9pt237s3jmc9sen6d0th582623e737s9hymqp55hwnfp3j3mhjcksktzqpx0qls6n4anc9nl82w7y5zpeztg4dxma6lmgw6dwj76v7drqn8tznl86jnrpvm3zfg3y4 +25_000,pizza,penumbra1csu9gn9x9pt237s3jmc9sen6d0th582623e737s9hymqp55hwnfp3j3mhjcksktzqpx0qls6n4anc9nl82w7y5zpeztg4dxma6lmgw6dwj76v7drqn8tznl86jnrpvm3zfg3y4 +250,cube,penumbra1csu9gn9x9pt237s3jmc9sen6d0th582623e737s9hymqp55hwnfp3j3mhjcksktzqpx0qls6n4anc9nl82w7y5zpeztg4dxma6lmgw6dwj76v7drqn8tznl86jnrpvm3zfg3y4 +1_000_000,test_usd,penumbra1csu9gn9x9pt237s3jmc9sen6d0th582623e737s9hymqp55hwnfp3j3mhjcksktzqpx0qls6n4anc9nl82w7y5zpeztg4dxma6lmgw6dwj76v7drqn8tznl86jnrpvm3zfg3y4 +10_000,nala,penumbra1csu9gn9x9pt237s3jmc9sen6d0th582623e737s9hymqp55hwnfp3j3mhjcksktzqpx0qls6n4anc9nl82w7y5zpeztg4dxma6lmgw6dwj76v7drqn8tznl86jnrpvm3zfg3y4 +5_000_000__000_000,upenumbra,penumbra1ufmv2ztuts8uuwa0lhn5v7fyp7tkrhseddkx54m538qknjrdyexj9kc5lez8s925vrqlkn724elqfq657gdpfr3z2qn3krhxx3zfpgnq43mct9zwzjjqscueml8w349php0aup +50_000,gm,penumbra1ufmv2ztuts8uuwa0lhn5v7fyp7tkrhseddkx54m538qknjrdyexj9kc5lez8s925vrqlkn724elqfq657gdpfr3z2qn3krhxx3zfpgnq43mct9zwzjjqscueml8w349php0aup +50_000,gn,penumbra1ufmv2ztuts8uuwa0lhn5v7fyp7tkrhseddkx54m538qknjrdyexj9kc5lez8s925vrqlkn724elqfq657gdpfr3z2qn3krhxx3zfpgnq43mct9zwzjjqscueml8w349php0aup +25_000,pizza,penumbra1ufmv2ztuts8uuwa0lhn5v7fyp7tkrhseddkx54m538qknjrdyexj9kc5lez8s925vrqlkn724elqfq657gdpfr3z2qn3krhxx3zfpgnq43mct9zwzjjqscueml8w349php0aup +250,cube,penumbra1ufmv2ztuts8uuwa0lhn5v7fyp7tkrhseddkx54m538qknjrdyexj9kc5lez8s925vrqlkn724elqfq657gdpfr3z2qn3krhxx3zfpgnq43mct9zwzjjqscueml8w349php0aup +1_000_000,test_usd,penumbra1ufmv2ztuts8uuwa0lhn5v7fyp7tkrhseddkx54m538qknjrdyexj9kc5lez8s925vrqlkn724elqfq657gdpfr3z2qn3krhxx3zfpgnq43mct9zwzjjqscueml8w349php0aup +10_000,nala,penumbra1ufmv2ztuts8uuwa0lhn5v7fyp7tkrhseddkx54m538qknjrdyexj9kc5lez8s925vrqlkn724elqfq657gdpfr3z2qn3krhxx3zfpgnq43mct9zwzjjqscueml8w349php0aup +5_000_000__000_000,upenumbra,penumbra1qu6svkqwyw5wu3huzuxcygurtvr7fek96akrwaypqq3sqggghaq6xdykx4gh8tvtgegqfyllrpmx4vhfpgq2sxmrfpv9zww5ugjxpfac7304hwhc3v7x0kw2wn9ng68nsejhz8 +50_000,gm,penumbra1qu6svkqwyw5wu3huzuxcygurtvr7fek96akrwaypqq3sqggghaq6xdykx4gh8tvtgegqfyllrpmx4vhfpgq2sxmrfpv9zww5ugjxpfac7304hwhc3v7x0kw2wn9ng68nsejhz8 +50_000,gn,penumbra1qu6svkqwyw5wu3huzuxcygurtvr7fek96akrwaypqq3sqggghaq6xdykx4gh8tvtgegqfyllrpmx4vhfpgq2sxmrfpv9zww5ugjxpfac7304hwhc3v7x0kw2wn9ng68nsejhz8 +25_000,pizza,penumbra1qu6svkqwyw5wu3huzuxcygurtvr7fek96akrwaypqq3sqggghaq6xdykx4gh8tvtgegqfyllrpmx4vhfpgq2sxmrfpv9zww5ugjxpfac7304hwhc3v7x0kw2wn9ng68nsejhz8 +250,cube,penumbra1qu6svkqwyw5wu3huzuxcygurtvr7fek96akrwaypqq3sqggghaq6xdykx4gh8tvtgegqfyllrpmx4vhfpgq2sxmrfpv9zww5ugjxpfac7304hwhc3v7x0kw2wn9ng68nsejhz8 +1_000_000,test_usd,penumbra1qu6svkqwyw5wu3huzuxcygurtvr7fek96akrwaypqq3sqggghaq6xdykx4gh8tvtgegqfyllrpmx4vhfpgq2sxmrfpv9zww5ugjxpfac7304hwhc3v7x0kw2wn9ng68nsejhz8 +10_000,nala,penumbra1qu6svkqwyw5wu3huzuxcygurtvr7fek96akrwaypqq3sqggghaq6xdykx4gh8tvtgegqfyllrpmx4vhfpgq2sxmrfpv9zww5ugjxpfac7304hwhc3v7x0kw2wn9ng68nsejhz8 +5_000_000__000_000,upenumbra,penumbra1ayr3xctcv7l8hqj3fq6fg9m5xnvpx5257dnc8axqeeaw7da8llenv53f4au2vyr8kzu7hh6ehlup5kxcnfqr7sgxzmtmu02qt9sxw2k97txa4x6crprlllvd0ly5ku5nd88cgg +50_000,gm,penumbra1ayr3xctcv7l8hqj3fq6fg9m5xnvpx5257dnc8axqeeaw7da8llenv53f4au2vyr8kzu7hh6ehlup5kxcnfqr7sgxzmtmu02qt9sxw2k97txa4x6crprlllvd0ly5ku5nd88cgg +50_000,gn,penumbra1ayr3xctcv7l8hqj3fq6fg9m5xnvpx5257dnc8axqeeaw7da8llenv53f4au2vyr8kzu7hh6ehlup5kxcnfqr7sgxzmtmu02qt9sxw2k97txa4x6crprlllvd0ly5ku5nd88cgg +25_000,pizza,penumbra1ayr3xctcv7l8hqj3fq6fg9m5xnvpx5257dnc8axqeeaw7da8llenv53f4au2vyr8kzu7hh6ehlup5kxcnfqr7sgxzmtmu02qt9sxw2k97txa4x6crprlllvd0ly5ku5nd88cgg +250,cube,penumbra1ayr3xctcv7l8hqj3fq6fg9m5xnvpx5257dnc8axqeeaw7da8llenv53f4au2vyr8kzu7hh6ehlup5kxcnfqr7sgxzmtmu02qt9sxw2k97txa4x6crprlllvd0ly5ku5nd88cgg +1_000_000,test_usd,penumbra1ayr3xctcv7l8hqj3fq6fg9m5xnvpx5257dnc8axqeeaw7da8llenv53f4au2vyr8kzu7hh6ehlup5kxcnfqr7sgxzmtmu02qt9sxw2k97txa4x6crprlllvd0ly5ku5nd88cgg +10_000,nala,penumbra1ayr3xctcv7l8hqj3fq6fg9m5xnvpx5257dnc8axqeeaw7da8llenv53f4au2vyr8kzu7hh6ehlup5kxcnfqr7sgxzmtmu02qt9sxw2k97txa4x6crprlllvd0ly5ku5nd88cgg +5_000_000__000_000,upenumbra,penumbra1ptthvmv380xzsph57ly92ctr88x285rce9adggsxgmwtve7n2q80lgea7q33gpxyfzevu36jk45sxh9muyj4kx7dhsasnzx8kass5p77j2vg5yp4gzpez3e6w0tqmpmwmr4s0w +50_000,gm,penumbra1ptthvmv380xzsph57ly92ctr88x285rce9adggsxgmwtve7n2q80lgea7q33gpxyfzevu36jk45sxh9muyj4kx7dhsasnzx8kass5p77j2vg5yp4gzpez3e6w0tqmpmwmr4s0w +50_000,gn,penumbra1ptthvmv380xzsph57ly92ctr88x285rce9adggsxgmwtve7n2q80lgea7q33gpxyfzevu36jk45sxh9muyj4kx7dhsasnzx8kass5p77j2vg5yp4gzpez3e6w0tqmpmwmr4s0w +25_000,pizza,penumbra1ptthvmv380xzsph57ly92ctr88x285rce9adggsxgmwtve7n2q80lgea7q33gpxyfzevu36jk45sxh9muyj4kx7dhsasnzx8kass5p77j2vg5yp4gzpez3e6w0tqmpmwmr4s0w +250,cube,penumbra1ptthvmv380xzsph57ly92ctr88x285rce9adggsxgmwtve7n2q80lgea7q33gpxyfzevu36jk45sxh9muyj4kx7dhsasnzx8kass5p77j2vg5yp4gzpez3e6w0tqmpmwmr4s0w +1_000_000,test_usd,penumbra1ptthvmv380xzsph57ly92ctr88x285rce9adggsxgmwtve7n2q80lgea7q33gpxyfzevu36jk45sxh9muyj4kx7dhsasnzx8kass5p77j2vg5yp4gzpez3e6w0tqmpmwmr4s0w +10_000,nala,penumbra1ptthvmv380xzsph57ly92ctr88x285rce9adggsxgmwtve7n2q80lgea7q33gpxyfzevu36jk45sxh9muyj4kx7dhsasnzx8kass5p77j2vg5yp4gzpez3e6w0tqmpmwmr4s0w +5_000_000__000_000,upenumbra,penumbra1jf62rvcyl7pz4373a52r7wyey4tanjwvstgnh8t6a9vatut6pg5xz89mvegjxr6jjxyzdvtfxlenalwcx2pc0pzv93czarx86wvtgs6k7xrjy3ra07wwkgjqzckxsx00g8kawf +50_000,gm,penumbra1jf62rvcyl7pz4373a52r7wyey4tanjwvstgnh8t6a9vatut6pg5xz89mvegjxr6jjxyzdvtfxlenalwcx2pc0pzv93czarx86wvtgs6k7xrjy3ra07wwkgjqzckxsx00g8kawf +50_000,gn,penumbra1jf62rvcyl7pz4373a52r7wyey4tanjwvstgnh8t6a9vatut6pg5xz89mvegjxr6jjxyzdvtfxlenalwcx2pc0pzv93czarx86wvtgs6k7xrjy3ra07wwkgjqzckxsx00g8kawf +25_000,pizza,penumbra1jf62rvcyl7pz4373a52r7wyey4tanjwvstgnh8t6a9vatut6pg5xz89mvegjxr6jjxyzdvtfxlenalwcx2pc0pzv93czarx86wvtgs6k7xrjy3ra07wwkgjqzckxsx00g8kawf +250,cube,penumbra1jf62rvcyl7pz4373a52r7wyey4tanjwvstgnh8t6a9vatut6pg5xz89mvegjxr6jjxyzdvtfxlenalwcx2pc0pzv93czarx86wvtgs6k7xrjy3ra07wwkgjqzckxsx00g8kawf +1_000_000,test_usd,penumbra1jf62rvcyl7pz4373a52r7wyey4tanjwvstgnh8t6a9vatut6pg5xz89mvegjxr6jjxyzdvtfxlenalwcx2pc0pzv93czarx86wvtgs6k7xrjy3ra07wwkgjqzckxsx00g8kawf +10_000,nala,penumbra1jf62rvcyl7pz4373a52r7wyey4tanjwvstgnh8t6a9vatut6pg5xz89mvegjxr6jjxyzdvtfxlenalwcx2pc0pzv93czarx86wvtgs6k7xrjy3ra07wwkgjqzckxsx00g8kawf +5_000_000__000_000,upenumbra,penumbra1grp3dk8vze54vq529lw8p5ankpk795hu5pg4zzjqaefggskqsehjaqsugh9uqavzytpmz8maxf4t3w49jl3ltaqakjejr85hw8qxnv8wlve0zhyhxk904q4yagjxgn9368ak8c +50_000,gm,penumbra1grp3dk8vze54vq529lw8p5ankpk795hu5pg4zzjqaefggskqsehjaqsugh9uqavzytpmz8maxf4t3w49jl3ltaqakjejr85hw8qxnv8wlve0zhyhxk904q4yagjxgn9368ak8c +50_000,gn,penumbra1grp3dk8vze54vq529lw8p5ankpk795hu5pg4zzjqaefggskqsehjaqsugh9uqavzytpmz8maxf4t3w49jl3ltaqakjejr85hw8qxnv8wlve0zhyhxk904q4yagjxgn9368ak8c +25_000,pizza,penumbra1grp3dk8vze54vq529lw8p5ankpk795hu5pg4zzjqaefggskqsehjaqsugh9uqavzytpmz8maxf4t3w49jl3ltaqakjejr85hw8qxnv8wlve0zhyhxk904q4yagjxgn9368ak8c +250,cube,penumbra1grp3dk8vze54vq529lw8p5ankpk795hu5pg4zzjqaefggskqsehjaqsugh9uqavzytpmz8maxf4t3w49jl3ltaqakjejr85hw8qxnv8wlve0zhyhxk904q4yagjxgn9368ak8c +1_000_000,test_usd,penumbra1grp3dk8vze54vq529lw8p5ankpk795hu5pg4zzjqaefggskqsehjaqsugh9uqavzytpmz8maxf4t3w49jl3ltaqakjejr85hw8qxnv8wlve0zhyhxk904q4yagjxgn9368ak8c +10_000,nala,penumbra1grp3dk8vze54vq529lw8p5ankpk795hu5pg4zzjqaefggskqsehjaqsugh9uqavzytpmz8maxf4t3w49jl3ltaqakjejr85hw8qxnv8wlve0zhyhxk904q4yagjxgn9368ak8c +5_000_000__000_000,upenumbra,penumbra18vddyvr33t8ukka0c94spdg63fh4yycwdtl9h3gpf4wfmnn5t2wp64xtmmlexlrjn90um2l6ne75evw8cvu27eufstsm5m7dfs740cw04grtmdsvqyc2vmqz3h4wu6ecg6ejrf +50_000,gm,penumbra18vddyvr33t8ukka0c94spdg63fh4yycwdtl9h3gpf4wfmnn5t2wp64xtmmlexlrjn90um2l6ne75evw8cvu27eufstsm5m7dfs740cw04grtmdsvqyc2vmqz3h4wu6ecg6ejrf +50_000,gn,penumbra18vddyvr33t8ukka0c94spdg63fh4yycwdtl9h3gpf4wfmnn5t2wp64xtmmlexlrjn90um2l6ne75evw8cvu27eufstsm5m7dfs740cw04grtmdsvqyc2vmqz3h4wu6ecg6ejrf +25_000,pizza,penumbra18vddyvr33t8ukka0c94spdg63fh4yycwdtl9h3gpf4wfmnn5t2wp64xtmmlexlrjn90um2l6ne75evw8cvu27eufstsm5m7dfs740cw04grtmdsvqyc2vmqz3h4wu6ecg6ejrf +250,cube,penumbra18vddyvr33t8ukka0c94spdg63fh4yycwdtl9h3gpf4wfmnn5t2wp64xtmmlexlrjn90um2l6ne75evw8cvu27eufstsm5m7dfs740cw04grtmdsvqyc2vmqz3h4wu6ecg6ejrf +1_000_000,test_usd,penumbra18vddyvr33t8ukka0c94spdg63fh4yycwdtl9h3gpf4wfmnn5t2wp64xtmmlexlrjn90um2l6ne75evw8cvu27eufstsm5m7dfs740cw04grtmdsvqyc2vmqz3h4wu6ecg6ejrf +10_000,nala,penumbra18vddyvr33t8ukka0c94spdg63fh4yycwdtl9h3gpf4wfmnn5t2wp64xtmmlexlrjn90um2l6ne75evw8cvu27eufstsm5m7dfs740cw04grtmdsvqyc2vmqz3h4wu6ecg6ejrf +5_000_000__000_000,upenumbra,penumbra1zx97wldrv7fku6p0mnuzr2kx4w2zurg869jvjh369vx5tf46krxh6nep0fc4mr82mmphq9w5jk69dav07ywym4ndndgszskfxkvzs6jke483fuqdr06n67kp86dj3er97tgs2p +50_000,gm,penumbra1zx97wldrv7fku6p0mnuzr2kx4w2zurg869jvjh369vx5tf46krxh6nep0fc4mr82mmphq9w5jk69dav07ywym4ndndgszskfxkvzs6jke483fuqdr06n67kp86dj3er97tgs2p +50_000,gn,penumbra1zx97wldrv7fku6p0mnuzr2kx4w2zurg869jvjh369vx5tf46krxh6nep0fc4mr82mmphq9w5jk69dav07ywym4ndndgszskfxkvzs6jke483fuqdr06n67kp86dj3er97tgs2p +25_000,pizza,penumbra1zx97wldrv7fku6p0mnuzr2kx4w2zurg869jvjh369vx5tf46krxh6nep0fc4mr82mmphq9w5jk69dav07ywym4ndndgszskfxkvzs6jke483fuqdr06n67kp86dj3er97tgs2p +250,cube,penumbra1zx97wldrv7fku6p0mnuzr2kx4w2zurg869jvjh369vx5tf46krxh6nep0fc4mr82mmphq9w5jk69dav07ywym4ndndgszskfxkvzs6jke483fuqdr06n67kp86dj3er97tgs2p +1_000_000,test_usd,penumbra1zx97wldrv7fku6p0mnuzr2kx4w2zurg869jvjh369vx5tf46krxh6nep0fc4mr82mmphq9w5jk69dav07ywym4ndndgszskfxkvzs6jke483fuqdr06n67kp86dj3er97tgs2p +10_000,nala,penumbra1zx97wldrv7fku6p0mnuzr2kx4w2zurg869jvjh369vx5tf46krxh6nep0fc4mr82mmphq9w5jk69dav07ywym4ndndgszskfxkvzs6jke483fuqdr06n67kp86dj3er97tgs2p +5_000_000__000_000,upenumbra,penumbra1s9lauumt0uceehtw03nvslann73vwtr2ytk2ej4vpdlrvxhmuze48hs08kguyyvj6td2epsr4y65wv7t3f5qwvmnx9tmpvltxck5jzycuvrumpv4dctq8339n85ep3la7kfvzs +50_000,gm,penumbra1s9lauumt0uceehtw03nvslann73vwtr2ytk2ej4vpdlrvxhmuze48hs08kguyyvj6td2epsr4y65wv7t3f5qwvmnx9tmpvltxck5jzycuvrumpv4dctq8339n85ep3la7kfvzs +50_000,gn,penumbra1s9lauumt0uceehtw03nvslann73vwtr2ytk2ej4vpdlrvxhmuze48hs08kguyyvj6td2epsr4y65wv7t3f5qwvmnx9tmpvltxck5jzycuvrumpv4dctq8339n85ep3la7kfvzs +25_000,pizza,penumbra1s9lauumt0uceehtw03nvslann73vwtr2ytk2ej4vpdlrvxhmuze48hs08kguyyvj6td2epsr4y65wv7t3f5qwvmnx9tmpvltxck5jzycuvrumpv4dctq8339n85ep3la7kfvzs +250,cube,penumbra1s9lauumt0uceehtw03nvslann73vwtr2ytk2ej4vpdlrvxhmuze48hs08kguyyvj6td2epsr4y65wv7t3f5qwvmnx9tmpvltxck5jzycuvrumpv4dctq8339n85ep3la7kfvzs +1_000_000,test_usd,penumbra1s9lauumt0uceehtw03nvslann73vwtr2ytk2ej4vpdlrvxhmuze48hs08kguyyvj6td2epsr4y65wv7t3f5qwvmnx9tmpvltxck5jzycuvrumpv4dctq8339n85ep3la7kfvzs +10_000,nala,penumbra1s9lauumt0uceehtw03nvslann73vwtr2ytk2ej4vpdlrvxhmuze48hs08kguyyvj6td2epsr4y65wv7t3f5qwvmnx9tmpvltxck5jzycuvrumpv4dctq8339n85ep3la7kfvzs +5_000_000__000_000,upenumbra,penumbra1khrsn8jqw8gxswqesq736zknfmtaeuccqrw4dlspn96hqnq80cxx32deke486tqf0644zxerkarwv8kwruh8yq45tegvavmc7uqj42cz77ecfhxcv0nn79vda60ccwsljxh34r +50_000,gm,penumbra1khrsn8jqw8gxswqesq736zknfmtaeuccqrw4dlspn96hqnq80cxx32deke486tqf0644zxerkarwv8kwruh8yq45tegvavmc7uqj42cz77ecfhxcv0nn79vda60ccwsljxh34r +50_000,gn,penumbra1khrsn8jqw8gxswqesq736zknfmtaeuccqrw4dlspn96hqnq80cxx32deke486tqf0644zxerkarwv8kwruh8yq45tegvavmc7uqj42cz77ecfhxcv0nn79vda60ccwsljxh34r +25_000,pizza,penumbra1khrsn8jqw8gxswqesq736zknfmtaeuccqrw4dlspn96hqnq80cxx32deke486tqf0644zxerkarwv8kwruh8yq45tegvavmc7uqj42cz77ecfhxcv0nn79vda60ccwsljxh34r +250,cube,penumbra1khrsn8jqw8gxswqesq736zknfmtaeuccqrw4dlspn96hqnq80cxx32deke486tqf0644zxerkarwv8kwruh8yq45tegvavmc7uqj42cz77ecfhxcv0nn79vda60ccwsljxh34r +1_000_000,test_usd,penumbra1khrsn8jqw8gxswqesq736zknfmtaeuccqrw4dlspn96hqnq80cxx32deke486tqf0644zxerkarwv8kwruh8yq45tegvavmc7uqj42cz77ecfhxcv0nn79vda60ccwsljxh34r +10_000,nala,penumbra1khrsn8jqw8gxswqesq736zknfmtaeuccqrw4dlspn96hqnq80cxx32deke486tqf0644zxerkarwv8kwruh8yq45tegvavmc7uqj42cz77ecfhxcv0nn79vda60ccwsljxh34r +5_000_000__000_000,upenumbra,penumbra1fuc2w752s2q4jzeayfhvvfusqndwplkz2hshuyr65cr9k33tnk8acc7crlfhkvqss9qan0j3q7cv6pagv44ljcu0jwk8g529n2yg539lnezjxajkmdf3fp2mq9rvjx9vv9es2a +50_000,gm,penumbra1fuc2w752s2q4jzeayfhvvfusqndwplkz2hshuyr65cr9k33tnk8acc7crlfhkvqss9qan0j3q7cv6pagv44ljcu0jwk8g529n2yg539lnezjxajkmdf3fp2mq9rvjx9vv9es2a +50_000,gn,penumbra1fuc2w752s2q4jzeayfhvvfusqndwplkz2hshuyr65cr9k33tnk8acc7crlfhkvqss9qan0j3q7cv6pagv44ljcu0jwk8g529n2yg539lnezjxajkmdf3fp2mq9rvjx9vv9es2a +25_000,pizza,penumbra1fuc2w752s2q4jzeayfhvvfusqndwplkz2hshuyr65cr9k33tnk8acc7crlfhkvqss9qan0j3q7cv6pagv44ljcu0jwk8g529n2yg539lnezjxajkmdf3fp2mq9rvjx9vv9es2a +250,cube,penumbra1fuc2w752s2q4jzeayfhvvfusqndwplkz2hshuyr65cr9k33tnk8acc7crlfhkvqss9qan0j3q7cv6pagv44ljcu0jwk8g529n2yg539lnezjxajkmdf3fp2mq9rvjx9vv9es2a +1_000_000,test_usd,penumbra1fuc2w752s2q4jzeayfhvvfusqndwplkz2hshuyr65cr9k33tnk8acc7crlfhkvqss9qan0j3q7cv6pagv44ljcu0jwk8g529n2yg539lnezjxajkmdf3fp2mq9rvjx9vv9es2a +10_000,nala,penumbra1fuc2w752s2q4jzeayfhvvfusqndwplkz2hshuyr65cr9k33tnk8acc7crlfhkvqss9qan0j3q7cv6pagv44ljcu0jwk8g529n2yg539lnezjxajkmdf3fp2mq9rvjx9vv9es2a +5_000_000__000_000,upenumbra,penumbra1n4y4k77hw5hl246mk8u4m7sw3muhgg6m4qmen3dvmz38vctw2htt4y4twwtmgpexcd7us6u7fa3xfmzst7yj3ur85mmmxas0xgj8d0aksqz9he3v4lhll0rmqgvxkgj827vuc0 +50_000,gm,penumbra1n4y4k77hw5hl246mk8u4m7sw3muhgg6m4qmen3dvmz38vctw2htt4y4twwtmgpexcd7us6u7fa3xfmzst7yj3ur85mmmxas0xgj8d0aksqz9he3v4lhll0rmqgvxkgj827vuc0 +50_000,gn,penumbra1n4y4k77hw5hl246mk8u4m7sw3muhgg6m4qmen3dvmz38vctw2htt4y4twwtmgpexcd7us6u7fa3xfmzst7yj3ur85mmmxas0xgj8d0aksqz9he3v4lhll0rmqgvxkgj827vuc0 +25_000,pizza,penumbra1n4y4k77hw5hl246mk8u4m7sw3muhgg6m4qmen3dvmz38vctw2htt4y4twwtmgpexcd7us6u7fa3xfmzst7yj3ur85mmmxas0xgj8d0aksqz9he3v4lhll0rmqgvxkgj827vuc0 +250,cube,penumbra1n4y4k77hw5hl246mk8u4m7sw3muhgg6m4qmen3dvmz38vctw2htt4y4twwtmgpexcd7us6u7fa3xfmzst7yj3ur85mmmxas0xgj8d0aksqz9he3v4lhll0rmqgvxkgj827vuc0 +1_000_000,test_usd,penumbra1n4y4k77hw5hl246mk8u4m7sw3muhgg6m4qmen3dvmz38vctw2htt4y4twwtmgpexcd7us6u7fa3xfmzst7yj3ur85mmmxas0xgj8d0aksqz9he3v4lhll0rmqgvxkgj827vuc0 +10_000,nala,penumbra1n4y4k77hw5hl246mk8u4m7sw3muhgg6m4qmen3dvmz38vctw2htt4y4twwtmgpexcd7us6u7fa3xfmzst7yj3ur85mmmxas0xgj8d0aksqz9he3v4lhll0rmqgvxkgj827vuc0 +5_000_000__000_000,upenumbra,penumbra1lr0ns0jwkdkzzthplehh4wga8qnwcys8gxn9wrgvxvg4ypxfd04s3y4kw0nz8apw99ny84yd4wcks0z6vt7djdrh3hnsxgewn3d354s757jxj6c47vufr023wlgy8zsphy2669 +50_000,gm,penumbra1lr0ns0jwkdkzzthplehh4wga8qnwcys8gxn9wrgvxvg4ypxfd04s3y4kw0nz8apw99ny84yd4wcks0z6vt7djdrh3hnsxgewn3d354s757jxj6c47vufr023wlgy8zsphy2669 +50_000,gn,penumbra1lr0ns0jwkdkzzthplehh4wga8qnwcys8gxn9wrgvxvg4ypxfd04s3y4kw0nz8apw99ny84yd4wcks0z6vt7djdrh3hnsxgewn3d354s757jxj6c47vufr023wlgy8zsphy2669 +25_000,pizza,penumbra1lr0ns0jwkdkzzthplehh4wga8qnwcys8gxn9wrgvxvg4ypxfd04s3y4kw0nz8apw99ny84yd4wcks0z6vt7djdrh3hnsxgewn3d354s757jxj6c47vufr023wlgy8zsphy2669 +250,cube,penumbra1lr0ns0jwkdkzzthplehh4wga8qnwcys8gxn9wrgvxvg4ypxfd04s3y4kw0nz8apw99ny84yd4wcks0z6vt7djdrh3hnsxgewn3d354s757jxj6c47vufr023wlgy8zsphy2669 +1_000_000,test_usd,penumbra1lr0ns0jwkdkzzthplehh4wga8qnwcys8gxn9wrgvxvg4ypxfd04s3y4kw0nz8apw99ny84yd4wcks0z6vt7djdrh3hnsxgewn3d354s757jxj6c47vufr023wlgy8zsphy2669 +10_000,nala,penumbra1lr0ns0jwkdkzzthplehh4wga8qnwcys8gxn9wrgvxvg4ypxfd04s3y4kw0nz8apw99ny84yd4wcks0z6vt7djdrh3hnsxgewn3d354s757jxj6c47vufr023wlgy8zsphy2669 +5_000_000__000_000,upenumbra,penumbra1uhe5hvhdf583s0ldyrurtatj78pa2tmzt0lcf9awgu4fezkrdaxsllmy39l59xqwxaqhx88ganfgkdnjqp8wmvf0s5uy4r9nmglyd497wsu7ny49jfth3dcjhhgs6d6mjc0fxq +50_000,gm,penumbra1uhe5hvhdf583s0ldyrurtatj78pa2tmzt0lcf9awgu4fezkrdaxsllmy39l59xqwxaqhx88ganfgkdnjqp8wmvf0s5uy4r9nmglyd497wsu7ny49jfth3dcjhhgs6d6mjc0fxq +50_000,gn,penumbra1uhe5hvhdf583s0ldyrurtatj78pa2tmzt0lcf9awgu4fezkrdaxsllmy39l59xqwxaqhx88ganfgkdnjqp8wmvf0s5uy4r9nmglyd497wsu7ny49jfth3dcjhhgs6d6mjc0fxq +25_000,pizza,penumbra1uhe5hvhdf583s0ldyrurtatj78pa2tmzt0lcf9awgu4fezkrdaxsllmy39l59xqwxaqhx88ganfgkdnjqp8wmvf0s5uy4r9nmglyd497wsu7ny49jfth3dcjhhgs6d6mjc0fxq +250,cube,penumbra1uhe5hvhdf583s0ldyrurtatj78pa2tmzt0lcf9awgu4fezkrdaxsllmy39l59xqwxaqhx88ganfgkdnjqp8wmvf0s5uy4r9nmglyd497wsu7ny49jfth3dcjhhgs6d6mjc0fxq +1_000_000,test_usd,penumbra1uhe5hvhdf583s0ldyrurtatj78pa2tmzt0lcf9awgu4fezkrdaxsllmy39l59xqwxaqhx88ganfgkdnjqp8wmvf0s5uy4r9nmglyd497wsu7ny49jfth3dcjhhgs6d6mjc0fxq +10_000,nala,penumbra1uhe5hvhdf583s0ldyrurtatj78pa2tmzt0lcf9awgu4fezkrdaxsllmy39l59xqwxaqhx88ganfgkdnjqp8wmvf0s5uy4r9nmglyd497wsu7ny49jfth3dcjhhgs6d6mjc0fxq +5_000_000__000_000,upenumbra,penumbra1fljn093ptt5vxyyd0pgxkqjdu7sqhutsz9q6r0h3fhedw25eta7plthmz80yyvprf4yue64htzts20l9398m6w0uqqe42cn5jefupqa4q5lcez27zwwv3z8aq87pl34r3dddkq +50_000,gm,penumbra1fljn093ptt5vxyyd0pgxkqjdu7sqhutsz9q6r0h3fhedw25eta7plthmz80yyvprf4yue64htzts20l9398m6w0uqqe42cn5jefupqa4q5lcez27zwwv3z8aq87pl34r3dddkq +50_000,gn,penumbra1fljn093ptt5vxyyd0pgxkqjdu7sqhutsz9q6r0h3fhedw25eta7plthmz80yyvprf4yue64htzts20l9398m6w0uqqe42cn5jefupqa4q5lcez27zwwv3z8aq87pl34r3dddkq +25_000,pizza,penumbra1fljn093ptt5vxyyd0pgxkqjdu7sqhutsz9q6r0h3fhedw25eta7plthmz80yyvprf4yue64htzts20l9398m6w0uqqe42cn5jefupqa4q5lcez27zwwv3z8aq87pl34r3dddkq +250,cube,penumbra1fljn093ptt5vxyyd0pgxkqjdu7sqhutsz9q6r0h3fhedw25eta7plthmz80yyvprf4yue64htzts20l9398m6w0uqqe42cn5jefupqa4q5lcez27zwwv3z8aq87pl34r3dddkq +1_000_000,test_usd,penumbra1fljn093ptt5vxyyd0pgxkqjdu7sqhutsz9q6r0h3fhedw25eta7plthmz80yyvprf4yue64htzts20l9398m6w0uqqe42cn5jefupqa4q5lcez27zwwv3z8aq87pl34r3dddkq +10_000,nala,penumbra1fljn093ptt5vxyyd0pgxkqjdu7sqhutsz9q6r0h3fhedw25eta7plthmz80yyvprf4yue64htzts20l9398m6w0uqqe42cn5jefupqa4q5lcez27zwwv3z8aq87pl34r3dddkq +5_000_000__000_000,upenumbra,penumbra13ftasxxac2c3288dsud8decvk98gql0lapgcelj4zd4nx6h9086zlt5pdlk7wnh799fr8e8wqyh082uf4x5jhk5wpl6p9smgwp8dkrvryha5cx2psq6s03utsu7ce3qyzl30at +50_000,gm,penumbra13ftasxxac2c3288dsud8decvk98gql0lapgcelj4zd4nx6h9086zlt5pdlk7wnh799fr8e8wqyh082uf4x5jhk5wpl6p9smgwp8dkrvryha5cx2psq6s03utsu7ce3qyzl30at +50_000,gn,penumbra13ftasxxac2c3288dsud8decvk98gql0lapgcelj4zd4nx6h9086zlt5pdlk7wnh799fr8e8wqyh082uf4x5jhk5wpl6p9smgwp8dkrvryha5cx2psq6s03utsu7ce3qyzl30at +25_000,pizza,penumbra13ftasxxac2c3288dsud8decvk98gql0lapgcelj4zd4nx6h9086zlt5pdlk7wnh799fr8e8wqyh082uf4x5jhk5wpl6p9smgwp8dkrvryha5cx2psq6s03utsu7ce3qyzl30at +250,cube,penumbra13ftasxxac2c3288dsud8decvk98gql0lapgcelj4zd4nx6h9086zlt5pdlk7wnh799fr8e8wqyh082uf4x5jhk5wpl6p9smgwp8dkrvryha5cx2psq6s03utsu7ce3qyzl30at +1_000_000,test_usd,penumbra13ftasxxac2c3288dsud8decvk98gql0lapgcelj4zd4nx6h9086zlt5pdlk7wnh799fr8e8wqyh082uf4x5jhk5wpl6p9smgwp8dkrvryha5cx2psq6s03utsu7ce3qyzl30at +10_000,nala,penumbra13ftasxxac2c3288dsud8decvk98gql0lapgcelj4zd4nx6h9086zlt5pdlk7wnh799fr8e8wqyh082uf4x5jhk5wpl6p9smgwp8dkrvryha5cx2psq6s03utsu7ce3qyzl30at +5_000_000__000_000,upenumbra,penumbra1mjnf0wpx7slv5ztzhk5fztg7xauack82w8ey6yqna5aqpygjap350qnrelpnlz5dv9zzczate02s4uu89tf87c6d85yatauhzg6rk08u25epzeu7efks704uqn4glx5dsqscer +50_000,gm,penumbra1mjnf0wpx7slv5ztzhk5fztg7xauack82w8ey6yqna5aqpygjap350qnrelpnlz5dv9zzczate02s4uu89tf87c6d85yatauhzg6rk08u25epzeu7efks704uqn4glx5dsqscer +50_000,gn,penumbra1mjnf0wpx7slv5ztzhk5fztg7xauack82w8ey6yqna5aqpygjap350qnrelpnlz5dv9zzczate02s4uu89tf87c6d85yatauhzg6rk08u25epzeu7efks704uqn4glx5dsqscer +25_000,pizza,penumbra1mjnf0wpx7slv5ztzhk5fztg7xauack82w8ey6yqna5aqpygjap350qnrelpnlz5dv9zzczate02s4uu89tf87c6d85yatauhzg6rk08u25epzeu7efks704uqn4glx5dsqscer +250,cube,penumbra1mjnf0wpx7slv5ztzhk5fztg7xauack82w8ey6yqna5aqpygjap350qnrelpnlz5dv9zzczate02s4uu89tf87c6d85yatauhzg6rk08u25epzeu7efks704uqn4glx5dsqscer +1_000_000,test_usd,penumbra1mjnf0wpx7slv5ztzhk5fztg7xauack82w8ey6yqna5aqpygjap350qnrelpnlz5dv9zzczate02s4uu89tf87c6d85yatauhzg6rk08u25epzeu7efks704uqn4glx5dsqscer +10_000,nala,penumbra1mjnf0wpx7slv5ztzhk5fztg7xauack82w8ey6yqna5aqpygjap350qnrelpnlz5dv9zzczate02s4uu89tf87c6d85yatauhzg6rk08u25epzeu7efks704uqn4glx5dsqscer +5_000_000__000_000,upenumbra,penumbra13fwnptqqpvq8r8v90phns04sewtdls8mtcce35sqffypdl8r8az0ayw926lnef508sk4wa9njxxfyp84tcexljgv0mq8vcz2fesrrth8wrkmwcj3q7k7jyk6lcet9pfwyvj5yp +50_000,gm,penumbra13fwnptqqpvq8r8v90phns04sewtdls8mtcce35sqffypdl8r8az0ayw926lnef508sk4wa9njxxfyp84tcexljgv0mq8vcz2fesrrth8wrkmwcj3q7k7jyk6lcet9pfwyvj5yp +50_000,gn,penumbra13fwnptqqpvq8r8v90phns04sewtdls8mtcce35sqffypdl8r8az0ayw926lnef508sk4wa9njxxfyp84tcexljgv0mq8vcz2fesrrth8wrkmwcj3q7k7jyk6lcet9pfwyvj5yp +25_000,pizza,penumbra13fwnptqqpvq8r8v90phns04sewtdls8mtcce35sqffypdl8r8az0ayw926lnef508sk4wa9njxxfyp84tcexljgv0mq8vcz2fesrrth8wrkmwcj3q7k7jyk6lcet9pfwyvj5yp +250,cube,penumbra13fwnptqqpvq8r8v90phns04sewtdls8mtcce35sqffypdl8r8az0ayw926lnef508sk4wa9njxxfyp84tcexljgv0mq8vcz2fesrrth8wrkmwcj3q7k7jyk6lcet9pfwyvj5yp +1_000_000,test_usd,penumbra13fwnptqqpvq8r8v90phns04sewtdls8mtcce35sqffypdl8r8az0ayw926lnef508sk4wa9njxxfyp84tcexljgv0mq8vcz2fesrrth8wrkmwcj3q7k7jyk6lcet9pfwyvj5yp +10_000,nala,penumbra13fwnptqqpvq8r8v90phns04sewtdls8mtcce35sqffypdl8r8az0ayw926lnef508sk4wa9njxxfyp84tcexljgv0mq8vcz2fesrrth8wrkmwcj3q7k7jyk6lcet9pfwyvj5yp +5_000_000__000_000,upenumbra,penumbra1zy3rrk6dk0r25kq6kfaecs55rlh8egdx2crs54anz70qtlx6jyyywv4g2ah8hh6pjftzr9w296zrjcagys4ztdhwfnjuvdj3js6ltmld48wwl4m0kgmr3asmdxw85ty79lddv0 +50_000,gm,penumbra1zy3rrk6dk0r25kq6kfaecs55rlh8egdx2crs54anz70qtlx6jyyywv4g2ah8hh6pjftzr9w296zrjcagys4ztdhwfnjuvdj3js6ltmld48wwl4m0kgmr3asmdxw85ty79lddv0 +50_000,gn,penumbra1zy3rrk6dk0r25kq6kfaecs55rlh8egdx2crs54anz70qtlx6jyyywv4g2ah8hh6pjftzr9w296zrjcagys4ztdhwfnjuvdj3js6ltmld48wwl4m0kgmr3asmdxw85ty79lddv0 +25_000,pizza,penumbra1zy3rrk6dk0r25kq6kfaecs55rlh8egdx2crs54anz70qtlx6jyyywv4g2ah8hh6pjftzr9w296zrjcagys4ztdhwfnjuvdj3js6ltmld48wwl4m0kgmr3asmdxw85ty79lddv0 +250,cube,penumbra1zy3rrk6dk0r25kq6kfaecs55rlh8egdx2crs54anz70qtlx6jyyywv4g2ah8hh6pjftzr9w296zrjcagys4ztdhwfnjuvdj3js6ltmld48wwl4m0kgmr3asmdxw85ty79lddv0 +1_000_000,test_usd,penumbra1zy3rrk6dk0r25kq6kfaecs55rlh8egdx2crs54anz70qtlx6jyyywv4g2ah8hh6pjftzr9w296zrjcagys4ztdhwfnjuvdj3js6ltmld48wwl4m0kgmr3asmdxw85ty79lddv0 +10_000,nala,penumbra1zy3rrk6dk0r25kq6kfaecs55rlh8egdx2crs54anz70qtlx6jyyywv4g2ah8hh6pjftzr9w296zrjcagys4ztdhwfnjuvdj3js6ltmld48wwl4m0kgmr3asmdxw85ty79lddv0 +5_000_000__000_000,upenumbra,penumbra1xuxvkfwszgu4s6s3c44kcpr40lawrddaknsqk705tfcea2fxvmk87kkurj7my4w92jraxrmfjlyvp0fqpyttpp5tqt96pw54nnndjglgmqjnjev9drtrmrx2x60ejr3sf08r82 +50_000,gm,penumbra1xuxvkfwszgu4s6s3c44kcpr40lawrddaknsqk705tfcea2fxvmk87kkurj7my4w92jraxrmfjlyvp0fqpyttpp5tqt96pw54nnndjglgmqjnjev9drtrmrx2x60ejr3sf08r82 +50_000,gn,penumbra1xuxvkfwszgu4s6s3c44kcpr40lawrddaknsqk705tfcea2fxvmk87kkurj7my4w92jraxrmfjlyvp0fqpyttpp5tqt96pw54nnndjglgmqjnjev9drtrmrx2x60ejr3sf08r82 +25_000,pizza,penumbra1xuxvkfwszgu4s6s3c44kcpr40lawrddaknsqk705tfcea2fxvmk87kkurj7my4w92jraxrmfjlyvp0fqpyttpp5tqt96pw54nnndjglgmqjnjev9drtrmrx2x60ejr3sf08r82 +250,cube,penumbra1xuxvkfwszgu4s6s3c44kcpr40lawrddaknsqk705tfcea2fxvmk87kkurj7my4w92jraxrmfjlyvp0fqpyttpp5tqt96pw54nnndjglgmqjnjev9drtrmrx2x60ejr3sf08r82 +1_000_000,test_usd,penumbra1xuxvkfwszgu4s6s3c44kcpr40lawrddaknsqk705tfcea2fxvmk87kkurj7my4w92jraxrmfjlyvp0fqpyttpp5tqt96pw54nnndjglgmqjnjev9drtrmrx2x60ejr3sf08r82 +10_000,nala,penumbra1xuxvkfwszgu4s6s3c44kcpr40lawrddaknsqk705tfcea2fxvmk87kkurj7my4w92jraxrmfjlyvp0fqpyttpp5tqt96pw54nnndjglgmqjnjev9drtrmrx2x60ejr3sf08r82 +5_000_000__000_000,upenumbra,penumbra10r8mu4dzf8v3a4k0q30h300e7y9pns0u76xhagqfwqr2wt6pqdgwjf4q3uq6wwn8mhas0w2a9qm3quqjk09yljetuwaf29x2r8jq69l93euhjhhfwkdrnk7x7zn7cfa2xjuhk5 +50_000,gm,penumbra10r8mu4dzf8v3a4k0q30h300e7y9pns0u76xhagqfwqr2wt6pqdgwjf4q3uq6wwn8mhas0w2a9qm3quqjk09yljetuwaf29x2r8jq69l93euhjhhfwkdrnk7x7zn7cfa2xjuhk5 +50_000,gn,penumbra10r8mu4dzf8v3a4k0q30h300e7y9pns0u76xhagqfwqr2wt6pqdgwjf4q3uq6wwn8mhas0w2a9qm3quqjk09yljetuwaf29x2r8jq69l93euhjhhfwkdrnk7x7zn7cfa2xjuhk5 +25_000,pizza,penumbra10r8mu4dzf8v3a4k0q30h300e7y9pns0u76xhagqfwqr2wt6pqdgwjf4q3uq6wwn8mhas0w2a9qm3quqjk09yljetuwaf29x2r8jq69l93euhjhhfwkdrnk7x7zn7cfa2xjuhk5 +250,cube,penumbra10r8mu4dzf8v3a4k0q30h300e7y9pns0u76xhagqfwqr2wt6pqdgwjf4q3uq6wwn8mhas0w2a9qm3quqjk09yljetuwaf29x2r8jq69l93euhjhhfwkdrnk7x7zn7cfa2xjuhk5 +1_000_000,test_usd,penumbra10r8mu4dzf8v3a4k0q30h300e7y9pns0u76xhagqfwqr2wt6pqdgwjf4q3uq6wwn8mhas0w2a9qm3quqjk09yljetuwaf29x2r8jq69l93euhjhhfwkdrnk7x7zn7cfa2xjuhk5 +10_000,nala,penumbra10r8mu4dzf8v3a4k0q30h300e7y9pns0u76xhagqfwqr2wt6pqdgwjf4q3uq6wwn8mhas0w2a9qm3quqjk09yljetuwaf29x2r8jq69l93euhjhhfwkdrnk7x7zn7cfa2xjuhk5 +5_000_000__000_000,upenumbra,penumbra1yw9pvm5ujry02yv7f67czu2pgqf36akpmvq90tk6f3h7pwn6y5vqnsg90ejn7zvu7rvmxd6stz4cqdazk0fvyurkvrkzau2wn275xaxfszsrd0mxqpegprdpu8wtjrk3snc2vx +50_000,gm,penumbra1yw9pvm5ujry02yv7f67czu2pgqf36akpmvq90tk6f3h7pwn6y5vqnsg90ejn7zvu7rvmxd6stz4cqdazk0fvyurkvrkzau2wn275xaxfszsrd0mxqpegprdpu8wtjrk3snc2vx +50_000,gn,penumbra1yw9pvm5ujry02yv7f67czu2pgqf36akpmvq90tk6f3h7pwn6y5vqnsg90ejn7zvu7rvmxd6stz4cqdazk0fvyurkvrkzau2wn275xaxfszsrd0mxqpegprdpu8wtjrk3snc2vx +25_000,pizza,penumbra1yw9pvm5ujry02yv7f67czu2pgqf36akpmvq90tk6f3h7pwn6y5vqnsg90ejn7zvu7rvmxd6stz4cqdazk0fvyurkvrkzau2wn275xaxfszsrd0mxqpegprdpu8wtjrk3snc2vx +250,cube,penumbra1yw9pvm5ujry02yv7f67czu2pgqf36akpmvq90tk6f3h7pwn6y5vqnsg90ejn7zvu7rvmxd6stz4cqdazk0fvyurkvrkzau2wn275xaxfszsrd0mxqpegprdpu8wtjrk3snc2vx +1_000_000,test_usd,penumbra1yw9pvm5ujry02yv7f67czu2pgqf36akpmvq90tk6f3h7pwn6y5vqnsg90ejn7zvu7rvmxd6stz4cqdazk0fvyurkvrkzau2wn275xaxfszsrd0mxqpegprdpu8wtjrk3snc2vx +10_000,nala,penumbra1yw9pvm5ujry02yv7f67czu2pgqf36akpmvq90tk6f3h7pwn6y5vqnsg90ejn7zvu7rvmxd6stz4cqdazk0fvyurkvrkzau2wn275xaxfszsrd0mxqpegprdpu8wtjrk3snc2vx +5_000_000__000_000,upenumbra,penumbra19ps33tjkfgntse69wewdgk2axx0tj5h23mvsw436h7pnnr3jnfxfksh0jw8hy0mssp4hpwkkh6pje5jfm4kkj2j59s3tdaweka9sywktqfkxkha6nzj36vatvfjzpglvxyh4xz +50_000,gm,penumbra19ps33tjkfgntse69wewdgk2axx0tj5h23mvsw436h7pnnr3jnfxfksh0jw8hy0mssp4hpwkkh6pje5jfm4kkj2j59s3tdaweka9sywktqfkxkha6nzj36vatvfjzpglvxyh4xz +50_000,gn,penumbra19ps33tjkfgntse69wewdgk2axx0tj5h23mvsw436h7pnnr3jnfxfksh0jw8hy0mssp4hpwkkh6pje5jfm4kkj2j59s3tdaweka9sywktqfkxkha6nzj36vatvfjzpglvxyh4xz +25_000,pizza,penumbra19ps33tjkfgntse69wewdgk2axx0tj5h23mvsw436h7pnnr3jnfxfksh0jw8hy0mssp4hpwkkh6pje5jfm4kkj2j59s3tdaweka9sywktqfkxkha6nzj36vatvfjzpglvxyh4xz +250,cube,penumbra19ps33tjkfgntse69wewdgk2axx0tj5h23mvsw436h7pnnr3jnfxfksh0jw8hy0mssp4hpwkkh6pje5jfm4kkj2j59s3tdaweka9sywktqfkxkha6nzj36vatvfjzpglvxyh4xz +1_000_000,test_usd,penumbra19ps33tjkfgntse69wewdgk2axx0tj5h23mvsw436h7pnnr3jnfxfksh0jw8hy0mssp4hpwkkh6pje5jfm4kkj2j59s3tdaweka9sywktqfkxkha6nzj36vatvfjzpglvxyh4xz +10_000,nala,penumbra19ps33tjkfgntse69wewdgk2axx0tj5h23mvsw436h7pnnr3jnfxfksh0jw8hy0mssp4hpwkkh6pje5jfm4kkj2j59s3tdaweka9sywktqfkxkha6nzj36vatvfjzpglvxyh4xz +5_000_000__000_000,upenumbra,penumbra1d3au0cyjjzhmlr9umplm8s5v8hmw9242r8ve2j9rsd7m72npw6snw8s4r0j4cur8n9c7l6mwf9884arxd5cmw9c8htmym46nap3v8vpmnf0kcyrj9q44wcgvcjyjaegvptwvk5 +50_000,gm,penumbra1d3au0cyjjzhmlr9umplm8s5v8hmw9242r8ve2j9rsd7m72npw6snw8s4r0j4cur8n9c7l6mwf9884arxd5cmw9c8htmym46nap3v8vpmnf0kcyrj9q44wcgvcjyjaegvptwvk5 +50_000,gn,penumbra1d3au0cyjjzhmlr9umplm8s5v8hmw9242r8ve2j9rsd7m72npw6snw8s4r0j4cur8n9c7l6mwf9884arxd5cmw9c8htmym46nap3v8vpmnf0kcyrj9q44wcgvcjyjaegvptwvk5 +25_000,pizza,penumbra1d3au0cyjjzhmlr9umplm8s5v8hmw9242r8ve2j9rsd7m72npw6snw8s4r0j4cur8n9c7l6mwf9884arxd5cmw9c8htmym46nap3v8vpmnf0kcyrj9q44wcgvcjyjaegvptwvk5 +250,cube,penumbra1d3au0cyjjzhmlr9umplm8s5v8hmw9242r8ve2j9rsd7m72npw6snw8s4r0j4cur8n9c7l6mwf9884arxd5cmw9c8htmym46nap3v8vpmnf0kcyrj9q44wcgvcjyjaegvptwvk5 +1_000_000,test_usd,penumbra1d3au0cyjjzhmlr9umplm8s5v8hmw9242r8ve2j9rsd7m72npw6snw8s4r0j4cur8n9c7l6mwf9884arxd5cmw9c8htmym46nap3v8vpmnf0kcyrj9q44wcgvcjyjaegvptwvk5 +10_000,nala,penumbra1d3au0cyjjzhmlr9umplm8s5v8hmw9242r8ve2j9rsd7m72npw6snw8s4r0j4cur8n9c7l6mwf9884arxd5cmw9c8htmym46nap3v8vpmnf0kcyrj9q44wcgvcjyjaegvptwvk5 +5_000_000__000_000,upenumbra,penumbra1r3jn4v5jgl4ed3a9wg065zvajzu7kgcpdmtejjj9a0shtu4wy507vfcpdyx5t8pevs2k99a86kd4r4rghu5f7kd0h8n8p389cp973w82n5ylamhr95d4qz9wftrn3cwu2pfgzu +50_000,gm,penumbra1r3jn4v5jgl4ed3a9wg065zvajzu7kgcpdmtejjj9a0shtu4wy507vfcpdyx5t8pevs2k99a86kd4r4rghu5f7kd0h8n8p389cp973w82n5ylamhr95d4qz9wftrn3cwu2pfgzu +50_000,gn,penumbra1r3jn4v5jgl4ed3a9wg065zvajzu7kgcpdmtejjj9a0shtu4wy507vfcpdyx5t8pevs2k99a86kd4r4rghu5f7kd0h8n8p389cp973w82n5ylamhr95d4qz9wftrn3cwu2pfgzu +25_000,pizza,penumbra1r3jn4v5jgl4ed3a9wg065zvajzu7kgcpdmtejjj9a0shtu4wy507vfcpdyx5t8pevs2k99a86kd4r4rghu5f7kd0h8n8p389cp973w82n5ylamhr95d4qz9wftrn3cwu2pfgzu +250,cube,penumbra1r3jn4v5jgl4ed3a9wg065zvajzu7kgcpdmtejjj9a0shtu4wy507vfcpdyx5t8pevs2k99a86kd4r4rghu5f7kd0h8n8p389cp973w82n5ylamhr95d4qz9wftrn3cwu2pfgzu +1_000_000,test_usd,penumbra1r3jn4v5jgl4ed3a9wg065zvajzu7kgcpdmtejjj9a0shtu4wy507vfcpdyx5t8pevs2k99a86kd4r4rghu5f7kd0h8n8p389cp973w82n5ylamhr95d4qz9wftrn3cwu2pfgzu +10_000,nala,penumbra1r3jn4v5jgl4ed3a9wg065zvajzu7kgcpdmtejjj9a0shtu4wy507vfcpdyx5t8pevs2k99a86kd4r4rghu5f7kd0h8n8p389cp973w82n5ylamhr95d4qz9wftrn3cwu2pfgzu +5_000_000__000_000,upenumbra,penumbra1zearguc6jdx29yevaxacyfvhwmzvah72lwfw8ejtjezcs7u7vxpz7etwujqq0zmh0aqlkcfx2nednv57vpzj5gc7dg6wcjshdr05rdfza4uzp6tp0lyj2xafd6msp3p62u3h0w +50_000,gm,penumbra1zearguc6jdx29yevaxacyfvhwmzvah72lwfw8ejtjezcs7u7vxpz7etwujqq0zmh0aqlkcfx2nednv57vpzj5gc7dg6wcjshdr05rdfza4uzp6tp0lyj2xafd6msp3p62u3h0w +50_000,gn,penumbra1zearguc6jdx29yevaxacyfvhwmzvah72lwfw8ejtjezcs7u7vxpz7etwujqq0zmh0aqlkcfx2nednv57vpzj5gc7dg6wcjshdr05rdfza4uzp6tp0lyj2xafd6msp3p62u3h0w +25_000,pizza,penumbra1zearguc6jdx29yevaxacyfvhwmzvah72lwfw8ejtjezcs7u7vxpz7etwujqq0zmh0aqlkcfx2nednv57vpzj5gc7dg6wcjshdr05rdfza4uzp6tp0lyj2xafd6msp3p62u3h0w +250,cube,penumbra1zearguc6jdx29yevaxacyfvhwmzvah72lwfw8ejtjezcs7u7vxpz7etwujqq0zmh0aqlkcfx2nednv57vpzj5gc7dg6wcjshdr05rdfza4uzp6tp0lyj2xafd6msp3p62u3h0w +1_000_000,test_usd,penumbra1zearguc6jdx29yevaxacyfvhwmzvah72lwfw8ejtjezcs7u7vxpz7etwujqq0zmh0aqlkcfx2nednv57vpzj5gc7dg6wcjshdr05rdfza4uzp6tp0lyj2xafd6msp3p62u3h0w +10_000,nala,penumbra1zearguc6jdx29yevaxacyfvhwmzvah72lwfw8ejtjezcs7u7vxpz7etwujqq0zmh0aqlkcfx2nednv57vpzj5gc7dg6wcjshdr05rdfza4uzp6tp0lyj2xafd6msp3p62u3h0w +5_000_000__000_000,upenumbra,penumbra1vw6j4uankvltjwz08khc7tx0u7fsgjc4x58skfvxfda079rqys73y88kwank6mncr5xewz4ahy7863s0flnshvqyaapxtcjv0wrmtpgdue9f8v9hp3m9k85rhhd59mzta53xvm +50_000,gm,penumbra1vw6j4uankvltjwz08khc7tx0u7fsgjc4x58skfvxfda079rqys73y88kwank6mncr5xewz4ahy7863s0flnshvqyaapxtcjv0wrmtpgdue9f8v9hp3m9k85rhhd59mzta53xvm +50_000,gn,penumbra1vw6j4uankvltjwz08khc7tx0u7fsgjc4x58skfvxfda079rqys73y88kwank6mncr5xewz4ahy7863s0flnshvqyaapxtcjv0wrmtpgdue9f8v9hp3m9k85rhhd59mzta53xvm +25_000,pizza,penumbra1vw6j4uankvltjwz08khc7tx0u7fsgjc4x58skfvxfda079rqys73y88kwank6mncr5xewz4ahy7863s0flnshvqyaapxtcjv0wrmtpgdue9f8v9hp3m9k85rhhd59mzta53xvm +250,cube,penumbra1vw6j4uankvltjwz08khc7tx0u7fsgjc4x58skfvxfda079rqys73y88kwank6mncr5xewz4ahy7863s0flnshvqyaapxtcjv0wrmtpgdue9f8v9hp3m9k85rhhd59mzta53xvm +1_000_000,test_usd,penumbra1vw6j4uankvltjwz08khc7tx0u7fsgjc4x58skfvxfda079rqys73y88kwank6mncr5xewz4ahy7863s0flnshvqyaapxtcjv0wrmtpgdue9f8v9hp3m9k85rhhd59mzta53xvm +10_000,nala,penumbra1vw6j4uankvltjwz08khc7tx0u7fsgjc4x58skfvxfda079rqys73y88kwank6mncr5xewz4ahy7863s0flnshvqyaapxtcjv0wrmtpgdue9f8v9hp3m9k85rhhd59mzta53xvm +5_000_000__000_000,upenumbra,penumbra1e2tn2yzapw8mxncn3uwjqe2k8fsdvdxu3pcswlwl80a8gf87glrq3mqstc7rlcxa7snk6vke8rrwzxytpf4w3xzhy2edhhuxde4u2z48w7a2f97xea3z5da9pk3h2j0axn294m +50_000,gm,penumbra1e2tn2yzapw8mxncn3uwjqe2k8fsdvdxu3pcswlwl80a8gf87glrq3mqstc7rlcxa7snk6vke8rrwzxytpf4w3xzhy2edhhuxde4u2z48w7a2f97xea3z5da9pk3h2j0axn294m +50_000,gn,penumbra1e2tn2yzapw8mxncn3uwjqe2k8fsdvdxu3pcswlwl80a8gf87glrq3mqstc7rlcxa7snk6vke8rrwzxytpf4w3xzhy2edhhuxde4u2z48w7a2f97xea3z5da9pk3h2j0axn294m +25_000,pizza,penumbra1e2tn2yzapw8mxncn3uwjqe2k8fsdvdxu3pcswlwl80a8gf87glrq3mqstc7rlcxa7snk6vke8rrwzxytpf4w3xzhy2edhhuxde4u2z48w7a2f97xea3z5da9pk3h2j0axn294m +250,cube,penumbra1e2tn2yzapw8mxncn3uwjqe2k8fsdvdxu3pcswlwl80a8gf87glrq3mqstc7rlcxa7snk6vke8rrwzxytpf4w3xzhy2edhhuxde4u2z48w7a2f97xea3z5da9pk3h2j0axn294m +1_000_000,test_usd,penumbra1e2tn2yzapw8mxncn3uwjqe2k8fsdvdxu3pcswlwl80a8gf87glrq3mqstc7rlcxa7snk6vke8rrwzxytpf4w3xzhy2edhhuxde4u2z48w7a2f97xea3z5da9pk3h2j0axn294m +10_000,nala,penumbra1e2tn2yzapw8mxncn3uwjqe2k8fsdvdxu3pcswlwl80a8gf87glrq3mqstc7rlcxa7snk6vke8rrwzxytpf4w3xzhy2edhhuxde4u2z48w7a2f97xea3z5da9pk3h2j0axn294m +5_000_000__000_000,upenumbra,penumbra1u3x4wdz7j8qfx7q5hte40u9gq788qz5zn4aqlr5kfzw3dxzxyktxjq2y0ma3tnd6ufwzszm25spg45my74fqn3qmeq9laj3cz3pr7eqym6f4gldsngjrwm22gf4damepgkqegk +50_000,gm,penumbra1u3x4wdz7j8qfx7q5hte40u9gq788qz5zn4aqlr5kfzw3dxzxyktxjq2y0ma3tnd6ufwzszm25spg45my74fqn3qmeq9laj3cz3pr7eqym6f4gldsngjrwm22gf4damepgkqegk +50_000,gn,penumbra1u3x4wdz7j8qfx7q5hte40u9gq788qz5zn4aqlr5kfzw3dxzxyktxjq2y0ma3tnd6ufwzszm25spg45my74fqn3qmeq9laj3cz3pr7eqym6f4gldsngjrwm22gf4damepgkqegk +25_000,pizza,penumbra1u3x4wdz7j8qfx7q5hte40u9gq788qz5zn4aqlr5kfzw3dxzxyktxjq2y0ma3tnd6ufwzszm25spg45my74fqn3qmeq9laj3cz3pr7eqym6f4gldsngjrwm22gf4damepgkqegk +250,cube,penumbra1u3x4wdz7j8qfx7q5hte40u9gq788qz5zn4aqlr5kfzw3dxzxyktxjq2y0ma3tnd6ufwzszm25spg45my74fqn3qmeq9laj3cz3pr7eqym6f4gldsngjrwm22gf4damepgkqegk +1_000_000,test_usd,penumbra1u3x4wdz7j8qfx7q5hte40u9gq788qz5zn4aqlr5kfzw3dxzxyktxjq2y0ma3tnd6ufwzszm25spg45my74fqn3qmeq9laj3cz3pr7eqym6f4gldsngjrwm22gf4damepgkqegk +10_000,nala,penumbra1u3x4wdz7j8qfx7q5hte40u9gq788qz5zn4aqlr5kfzw3dxzxyktxjq2y0ma3tnd6ufwzszm25spg45my74fqn3qmeq9laj3cz3pr7eqym6f4gldsngjrwm22gf4damepgkqegk +5_000_000__000_000,upenumbra,penumbra1l6kpj6e5d7ph0nnggxwgwhk5wumm7k2vjulq0jysdvh7y2p0r0qe5dr7jntl8wz6xjt08dqvu8dnn49fnxhta5mfcdl0kjpruhj76umulw2dssslansy7qv6k3ajeh72qz2all +50_000,gm,penumbra1l6kpj6e5d7ph0nnggxwgwhk5wumm7k2vjulq0jysdvh7y2p0r0qe5dr7jntl8wz6xjt08dqvu8dnn49fnxhta5mfcdl0kjpruhj76umulw2dssslansy7qv6k3ajeh72qz2all +50_000,gn,penumbra1l6kpj6e5d7ph0nnggxwgwhk5wumm7k2vjulq0jysdvh7y2p0r0qe5dr7jntl8wz6xjt08dqvu8dnn49fnxhta5mfcdl0kjpruhj76umulw2dssslansy7qv6k3ajeh72qz2all +25_000,pizza,penumbra1l6kpj6e5d7ph0nnggxwgwhk5wumm7k2vjulq0jysdvh7y2p0r0qe5dr7jntl8wz6xjt08dqvu8dnn49fnxhta5mfcdl0kjpruhj76umulw2dssslansy7qv6k3ajeh72qz2all +250,cube,penumbra1l6kpj6e5d7ph0nnggxwgwhk5wumm7k2vjulq0jysdvh7y2p0r0qe5dr7jntl8wz6xjt08dqvu8dnn49fnxhta5mfcdl0kjpruhj76umulw2dssslansy7qv6k3ajeh72qz2all +1_000_000,test_usd,penumbra1l6kpj6e5d7ph0nnggxwgwhk5wumm7k2vjulq0jysdvh7y2p0r0qe5dr7jntl8wz6xjt08dqvu8dnn49fnxhta5mfcdl0kjpruhj76umulw2dssslansy7qv6k3ajeh72qz2all +10_000,nala,penumbra1l6kpj6e5d7ph0nnggxwgwhk5wumm7k2vjulq0jysdvh7y2p0r0qe5dr7jntl8wz6xjt08dqvu8dnn49fnxhta5mfcdl0kjpruhj76umulw2dssslansy7qv6k3ajeh72qz2all +5_000_000__000_000,upenumbra,penumbra12hcj6z3xxk4an08msjf2fu0qk90nv6582lqzlmjwnnd75xz6u928wrcw8vpqxd489d6nqyvf7raq7jv75hg3qsa0ut9j2sxhfqfwrh56v064xh7hemdp898mls38v8ca76et3x +50_000,gm,penumbra12hcj6z3xxk4an08msjf2fu0qk90nv6582lqzlmjwnnd75xz6u928wrcw8vpqxd489d6nqyvf7raq7jv75hg3qsa0ut9j2sxhfqfwrh56v064xh7hemdp898mls38v8ca76et3x +50_000,gn,penumbra12hcj6z3xxk4an08msjf2fu0qk90nv6582lqzlmjwnnd75xz6u928wrcw8vpqxd489d6nqyvf7raq7jv75hg3qsa0ut9j2sxhfqfwrh56v064xh7hemdp898mls38v8ca76et3x +25_000,pizza,penumbra12hcj6z3xxk4an08msjf2fu0qk90nv6582lqzlmjwnnd75xz6u928wrcw8vpqxd489d6nqyvf7raq7jv75hg3qsa0ut9j2sxhfqfwrh56v064xh7hemdp898mls38v8ca76et3x +250,cube,penumbra12hcj6z3xxk4an08msjf2fu0qk90nv6582lqzlmjwnnd75xz6u928wrcw8vpqxd489d6nqyvf7raq7jv75hg3qsa0ut9j2sxhfqfwrh56v064xh7hemdp898mls38v8ca76et3x +1_000_000,test_usd,penumbra12hcj6z3xxk4an08msjf2fu0qk90nv6582lqzlmjwnnd75xz6u928wrcw8vpqxd489d6nqyvf7raq7jv75hg3qsa0ut9j2sxhfqfwrh56v064xh7hemdp898mls38v8ca76et3x +10_000,nala,penumbra12hcj6z3xxk4an08msjf2fu0qk90nv6582lqzlmjwnnd75xz6u928wrcw8vpqxd489d6nqyvf7raq7jv75hg3qsa0ut9j2sxhfqfwrh56v064xh7hemdp898mls38v8ca76et3x +5_000_000__000_000,upenumbra,penumbra1nzyf0qh28727tv56wcyj4v5c8ur9vfel52uxmpmypzs7dyn5986ng3j9l3lnuyj73cls5dgrhn7exe7j9cp2kx6rnf6ggus6s3w2kkd8kf6ydr5wfplw34vzxrk9e8n20ckm70 +50_000,gm,penumbra1nzyf0qh28727tv56wcyj4v5c8ur9vfel52uxmpmypzs7dyn5986ng3j9l3lnuyj73cls5dgrhn7exe7j9cp2kx6rnf6ggus6s3w2kkd8kf6ydr5wfplw34vzxrk9e8n20ckm70 +50_000,gn,penumbra1nzyf0qh28727tv56wcyj4v5c8ur9vfel52uxmpmypzs7dyn5986ng3j9l3lnuyj73cls5dgrhn7exe7j9cp2kx6rnf6ggus6s3w2kkd8kf6ydr5wfplw34vzxrk9e8n20ckm70 +25_000,pizza,penumbra1nzyf0qh28727tv56wcyj4v5c8ur9vfel52uxmpmypzs7dyn5986ng3j9l3lnuyj73cls5dgrhn7exe7j9cp2kx6rnf6ggus6s3w2kkd8kf6ydr5wfplw34vzxrk9e8n20ckm70 +250,cube,penumbra1nzyf0qh28727tv56wcyj4v5c8ur9vfel52uxmpmypzs7dyn5986ng3j9l3lnuyj73cls5dgrhn7exe7j9cp2kx6rnf6ggus6s3w2kkd8kf6ydr5wfplw34vzxrk9e8n20ckm70 +1_000_000,test_usd,penumbra1nzyf0qh28727tv56wcyj4v5c8ur9vfel52uxmpmypzs7dyn5986ng3j9l3lnuyj73cls5dgrhn7exe7j9cp2kx6rnf6ggus6s3w2kkd8kf6ydr5wfplw34vzxrk9e8n20ckm70 +10_000,nala,penumbra1nzyf0qh28727tv56wcyj4v5c8ur9vfel52uxmpmypzs7dyn5986ng3j9l3lnuyj73cls5dgrhn7exe7j9cp2kx6rnf6ggus6s3w2kkd8kf6ydr5wfplw34vzxrk9e8n20ckm70 +5_000_000__000_000,upenumbra,penumbra1kllhj0jprucna84r8pnwsngccr0dl2erdra3sa75mhvprr688e98j2cl9j7ls9njvufcwhg03jzm8zea3rmgaevk2y42yy2gzxq4qgpm4cnx5xe4wz2qj4f3ramhxrpqspju04 +50_000,gm,penumbra1kllhj0jprucna84r8pnwsngccr0dl2erdra3sa75mhvprr688e98j2cl9j7ls9njvufcwhg03jzm8zea3rmgaevk2y42yy2gzxq4qgpm4cnx5xe4wz2qj4f3ramhxrpqspju04 +50_000,gn,penumbra1kllhj0jprucna84r8pnwsngccr0dl2erdra3sa75mhvprr688e98j2cl9j7ls9njvufcwhg03jzm8zea3rmgaevk2y42yy2gzxq4qgpm4cnx5xe4wz2qj4f3ramhxrpqspju04 +25_000,pizza,penumbra1kllhj0jprucna84r8pnwsngccr0dl2erdra3sa75mhvprr688e98j2cl9j7ls9njvufcwhg03jzm8zea3rmgaevk2y42yy2gzxq4qgpm4cnx5xe4wz2qj4f3ramhxrpqspju04 +250,cube,penumbra1kllhj0jprucna84r8pnwsngccr0dl2erdra3sa75mhvprr688e98j2cl9j7ls9njvufcwhg03jzm8zea3rmgaevk2y42yy2gzxq4qgpm4cnx5xe4wz2qj4f3ramhxrpqspju04 +1_000_000,test_usd,penumbra1kllhj0jprucna84r8pnwsngccr0dl2erdra3sa75mhvprr688e98j2cl9j7ls9njvufcwhg03jzm8zea3rmgaevk2y42yy2gzxq4qgpm4cnx5xe4wz2qj4f3ramhxrpqspju04 +10_000,nala,penumbra1kllhj0jprucna84r8pnwsngccr0dl2erdra3sa75mhvprr688e98j2cl9j7ls9njvufcwhg03jzm8zea3rmgaevk2y42yy2gzxq4qgpm4cnx5xe4wz2qj4f3ramhxrpqspju04 +10_000_000__000_000,upenumbra,penumbra1rqcd3hfvkvc04c4c9vc0ac87lh4y0z8l28k4xp6d0cnd5jc6f6k0neuzp6zdwtpwyfpswtdzv9jzqtpjn5t6wh96pfx3flq2dhqgc42u7c06kj57dl39w2xm6tg0wh4zc8kjjk +10_000_000,gm,penumbra1rqcd3hfvkvc04c4c9vc0ac87lh4y0z8l28k4xp6d0cnd5jc6f6k0neuzp6zdwtpwyfpswtdzv9jzqtpjn5t6wh96pfx3flq2dhqgc42u7c06kj57dl39w2xm6tg0wh4zc8kjjk +10_000_000,gn,penumbra1rqcd3hfvkvc04c4c9vc0ac87lh4y0z8l28k4xp6d0cnd5jc6f6k0neuzp6zdwtpwyfpswtdzv9jzqtpjn5t6wh96pfx3flq2dhqgc42u7c06kj57dl39w2xm6tg0wh4zc8kjjk +10_000_000,pizza,penumbra1rqcd3hfvkvc04c4c9vc0ac87lh4y0z8l28k4xp6d0cnd5jc6f6k0neuzp6zdwtpwyfpswtdzv9jzqtpjn5t6wh96pfx3flq2dhqgc42u7c06kj57dl39w2xm6tg0wh4zc8kjjk +10_000_000,cube,penumbra1rqcd3hfvkvc04c4c9vc0ac87lh4y0z8l28k4xp6d0cnd5jc6f6k0neuzp6zdwtpwyfpswtdzv9jzqtpjn5t6wh96pfx3flq2dhqgc42u7c06kj57dl39w2xm6tg0wh4zc8kjjk +10_000_000,test_usd,penumbra1rqcd3hfvkvc04c4c9vc0ac87lh4y0z8l28k4xp6d0cnd5jc6f6k0neuzp6zdwtpwyfpswtdzv9jzqtpjn5t6wh96pfx3flq2dhqgc42u7c06kj57dl39w2xm6tg0wh4zc8kjjk +10_000_000,test_btc,penumbra1rqcd3hfvkvc04c4c9vc0ac87lh4y0z8l28k4xp6d0cnd5jc6f6k0neuzp6zdwtpwyfpswtdzv9jzqtpjn5t6wh96pfx3flq2dhqgc42u7c06kj57dl39w2xm6tg0wh4zc8kjjk +10_000_000,test_eth,penumbra1rqcd3hfvkvc04c4c9vc0ac87lh4y0z8l28k4xp6d0cnd5jc6f6k0neuzp6zdwtpwyfpswtdzv9jzqtpjn5t6wh96pfx3flq2dhqgc42u7c06kj57dl39w2xm6tg0wh4zc8kjjk +10_000_000,test_atom,penumbra1rqcd3hfvkvc04c4c9vc0ac87lh4y0z8l28k4xp6d0cnd5jc6f6k0neuzp6zdwtpwyfpswtdzv9jzqtpjn5t6wh96pfx3flq2dhqgc42u7c06kj57dl39w2xm6tg0wh4zc8kjjk +10_000_000,test_osmo,penumbra1rqcd3hfvkvc04c4c9vc0ac87lh4y0z8l28k4xp6d0cnd5jc6f6k0neuzp6zdwtpwyfpswtdzv9jzqtpjn5t6wh96pfx3flq2dhqgc42u7c06kj57dl39w2xm6tg0wh4zc8kjjk +10_000_000__000_000,upenumbra,penumbra105shuajmml02qgfwmxz7rqqcjwu8tzjdyd3u75za35cj0se3pv5ssmxlv9qc86hawtae35whfst78g30z9xhyxvh8vrz433rlvslzg5675rwr8rcz7g6pszmc5tkm63u6kqa3f +10_000_000,gm,penumbra105shuajmml02qgfwmxz7rqqcjwu8tzjdyd3u75za35cj0se3pv5ssmxlv9qc86hawtae35whfst78g30z9xhyxvh8vrz433rlvslzg5675rwr8rcz7g6pszmc5tkm63u6kqa3f +10_000_000,gn,penumbra105shuajmml02qgfwmxz7rqqcjwu8tzjdyd3u75za35cj0se3pv5ssmxlv9qc86hawtae35whfst78g30z9xhyxvh8vrz433rlvslzg5675rwr8rcz7g6pszmc5tkm63u6kqa3f +10_000_000,pizza,penumbra105shuajmml02qgfwmxz7rqqcjwu8tzjdyd3u75za35cj0se3pv5ssmxlv9qc86hawtae35whfst78g30z9xhyxvh8vrz433rlvslzg5675rwr8rcz7g6pszmc5tkm63u6kqa3f +10_000_000,cube,penumbra105shuajmml02qgfwmxz7rqqcjwu8tzjdyd3u75za35cj0se3pv5ssmxlv9qc86hawtae35whfst78g30z9xhyxvh8vrz433rlvslzg5675rwr8rcz7g6pszmc5tkm63u6kqa3f +10_000_000,test_usd,penumbra105shuajmml02qgfwmxz7rqqcjwu8tzjdyd3u75za35cj0se3pv5ssmxlv9qc86hawtae35whfst78g30z9xhyxvh8vrz433rlvslzg5675rwr8rcz7g6pszmc5tkm63u6kqa3f +10_000_000,test_btc,penumbra105shuajmml02qgfwmxz7rqqcjwu8tzjdyd3u75za35cj0se3pv5ssmxlv9qc86hawtae35whfst78g30z9xhyxvh8vrz433rlvslzg5675rwr8rcz7g6pszmc5tkm63u6kqa3f +10_000_000,test_eth,penumbra105shuajmml02qgfwmxz7rqqcjwu8tzjdyd3u75za35cj0se3pv5ssmxlv9qc86hawtae35whfst78g30z9xhyxvh8vrz433rlvslzg5675rwr8rcz7g6pszmc5tkm63u6kqa3f +10_000_000,test_atom,penumbra105shuajmml02qgfwmxz7rqqcjwu8tzjdyd3u75za35cj0se3pv5ssmxlv9qc86hawtae35whfst78g30z9xhyxvh8vrz433rlvslzg5675rwr8rcz7g6pszmc5tkm63u6kqa3f +10_000_000,test_osmo,penumbra105shuajmml02qgfwmxz7rqqcjwu8tzjdyd3u75za35cj0se3pv5ssmxlv9qc86hawtae35whfst78g30z9xhyxvh8vrz433rlvslzg5675rwr8rcz7g6pszmc5tkm63u6kqa3f +100,gm,penumbra147mfall0zr6am5r45qkwht7xqqrdsp50czde7empv7yq2nk3z8yyfh9k9520ddgswkmzar22vhz9dwtuem7uxw0qytfpv7lk3q9dp8ccaw2fn5c838rfackazmgf3ahh09cxmz +5001,test_usd,penumbra147mfall0zr6am5r45qkwht7xqqrdsp50czde7empv7yq2nk3z8yyfh9k9520ddgswkmzar22vhz9dwtuem7uxw0qytfpv7lk3q9dp8ccaw2fn5c838rfackazmgf3ahh09cxmz +1,cube,penumbra147mfall0zr6am5r45qkwht7xqqrdsp50czde7empv7yq2nk3z8yyfh9k9520ddgswkmzar22vhz9dwtuem7uxw0qytfpv7lk3q9dp8ccaw2fn5c838rfackazmgf3ahh09cxmz +2_000__000_000,upenumbra,penumbra147mfall0zr6am5r45qkwht7xqqrdsp50czde7empv7yq2nk3z8yyfh9k9520ddgswkmzar22vhz9dwtuem7uxw0qytfpv7lk3q9dp8ccaw2fn5c838rfackazmgf3ahh09cxmz +1_000,test_usd,penumbra1vmmz304hjlkjq6xv4al5dqumvgk3ek82rneagj07vdqkudjvl6y7zxzr5k6qq24yc7yyyekpu9qm7ef3acg2u8p950hs6hu3e73guq5pfmmvm63qudfx4qmg8h7fdweyw3ektn +1_000__000_000,upenumbra,penumbra1vmmz304hjlkjq6xv4al5dqumvgk3ek82rneagj07vdqkudjvl6y7zxzr5k6qq24yc7yyyekpu9qm7ef3acg2u8p950hs6hu3e73guq5pfmmvm63qudfx4qmg8h7fdweyw3ektn diff --git a/testnets/070-deimos-6/validators.json b/testnets/070-deimos-6/validators.json new file mode 100644 index 0000000000..29bdf9f0ce --- /dev/null +++ b/testnets/070-deimos-6/validators.json @@ -0,0 +1,34 @@ +[ + { + "name": "Penumbra Labs CI 1", + "website": "https://penumbra.zone", + "description": "This is a validator run by Penumbra Labs, using testnets as a public CI", + "funding_streams": [ + [ + 50, + "penumbra1fcy6crf6u4r450k8y4nye43puxet2ytfh7s0dzxsxjk68czej9mp37xv49np0clv4dc8cwg4re0xfs79uwlfehnja4p0revmlek0drezxfse8spg3qc6gux6vyuzuulse7xuxv" + ], + [ + 50, + "penumbra13ahs2s8ms6q0utgetty3zflwteepg87gqm88sqqcdj2mjhhydkykwu6n7dk557x84aa9a6cqhdytw0zk33xjgmuedprrlunc86up6zps8juej9rpuuydjtk7jaxpmrw27gfu9x" + ], + [ + 50, + "penumbra1uw03wyt49u7wm5wgu4nvkdt0v48fdaw5y4az4xlgmnp6ucs6th4xd0zg8wqxwndwfv286ktjwgemyhrxqu0d5qjf8dapr57l3k8yqs09vw9m5ywxsx9hjj2dj4qwnrl2rzxdh9" + ], + [ + 50, + "penumbra1w6em8sdx0467ug9kk0s0sng254tqjfk9gglv6ff7dq2v8arwekevkjte9udzmsj9l83mz74747tj0a49w2vhecxj7ac4upr5c5pvjqhsy7dwn422m8dgdekt7y4lmad026njsv" + ], + [ + 50, + "penumbra1jp4pryqqmh65pq8e7zwk6k2674vwhn4qqphxjk0vukxln0crmp2tdld0mhavuyrspwuajnsk5t5t33u2auxvheunr7qde4l068ez0euvtu08z7rwj6shlh64ndz0wvz7cmu29z" + ], + [ + 50, + "penumbra1hum845ches70c8kp8zfx7nerjwfe653hxsrpgwepwtspcp4jy6ytnxhe5kwn56sku684x6zzqcwp5ycrkee5mmg9kdl3jkr5lqn2xq3kqxvp4d7gwqdue5jznk2ter2teg83gu" + ] + ], + "sequence_number": 0 + } +] \ No newline at end of file From c7cadd5dcc67bd6a3aac91ca2d51197fd1671b67 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Sat, 23 Mar 2024 16:42:43 -0400 Subject: [PATCH 004/168] cnidarium: use incremental jmt migration (#4053) Close #3506, this PR: - use `jmt@0.10` which includes the `migration` feature - bump `borsh` to `1.3.0` to be able to serialize jmt nodes - revamp the substore migration tests to include basic tests and a proptest strategy - notably, the proptest integration tests lay the groundwork for deeper/more targeted strategies --- Cargo.lock | 121 ++- Cargo.toml | 2 +- crates/cnidarium/Cargo.toml | 11 +- crates/cnidarium/src/storage.rs | 29 +- crates/cnidarium/src/store/multistore.rs | 5 +- crates/cnidarium/src/store/substore.rs | 30 +- crates/cnidarium/tests/migration.rs | 1073 ++++++++++++++++++++-- 7 files changed, 1146 insertions(+), 125 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 58f0996b66..7b21112819 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1093,47 +1093,26 @@ dependencies = [ [[package]] name = "borsh" -version = "0.10.3" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" +checksum = "f58b559fd6448c6e2fd0adb5720cd98a2506594cafa4737ff98c396f3e82f667" dependencies = [ "borsh-derive", - "hashbrown 0.13.2", + "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" -dependencies = [ - "borsh-derive-internal", - "borsh-schema-derive-internal", - "proc-macro-crate 0.1.5", - "proc-macro2 1.0.78", - "syn 1.0.109", -] - -[[package]] -name = "borsh-derive-internal" -version = "0.10.3" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" -dependencies = [ - "proc-macro2 1.0.78", - "quote 1.0.35", - "syn 1.0.109", -] - -[[package]] -name = "borsh-schema-derive-internal" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" +checksum = "7aadb5b6ccbd078890f6d7003694e33816e6b784358f18e15e7e6d9f065a57cd" dependencies = [ + "once_cell", + "proc-macro-crate 3.1.0", "proc-macro2 1.0.78", "quote 1.0.35", - "syn 1.0.109", + "syn 2.0.51", + "syn_derive", ] [[package]] @@ -1246,6 +1225,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chacha20" version = "0.8.2" @@ -1409,6 +1394,7 @@ dependencies = [ "parking_lot", "pbjson", "pin-project", + "proptest", "prost", "regex", "rocksdb", @@ -1417,6 +1403,7 @@ dependencies = [ "smallvec", "tempfile", "tendermint", + "test-strategy", "tokio", "tokio-stream", "tonic", @@ -3602,9 +3589,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jmt" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2950721a65dff82492e30fe67076127135d0d710aa0140f21efafda2aee7849" +checksum = "a9a3bf1a303934c6f75533bd3a563730a0730f9361023c49ed6aee9fcb5b98f8" dependencies = [ "anyhow", "borsh", @@ -6144,20 +6131,20 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "0.1.5" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "toml 0.5.11", + "toml_edit 0.20.7", ] [[package]] name = "proc-macro-crate" -version = "2.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" dependencies = [ - "toml_edit 0.20.7", + "toml_edit 0.21.1", ] [[package]] @@ -7538,6 +7525,29 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "structmeta" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ad9e09554f0456d67a69c1584c9798ba733a5b50349a6c0d0948710523922d" +dependencies = [ + "proc-macro2 1.0.78", + "quote 1.0.35", + "structmeta-derive", + "syn 2.0.51", +] + +[[package]] +name = "structmeta-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00" +dependencies = [ + "proc-macro2 1.0.78", + "quote 1.0.35", + "syn 2.0.51", +] + [[package]] name = "strum" version = "0.23.0" @@ -7662,6 +7672,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error 1.0.4", + "proc-macro2 1.0.78", + "quote 1.0.35", + "syn 2.0.51", +] + [[package]] name = "sync_wrapper" version = "0.1.2" @@ -7841,6 +7863,18 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +[[package]] +name = "test-strategy" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8361c808554228ad09bfed70f5c823caf8a3450b6881cc3a38eb57e8c08c1d9" +dependencies = [ + "proc-macro2 1.0.78", + "quote 1.0.35", + "structmeta", + "syn 2.0.51", +] + [[package]] name = "textwrap" version = "0.16.1" @@ -8142,6 +8176,17 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap 2.2.3", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.10.2" diff --git a/Cargo.toml b/Cargo.toml index 5b1aa172af..8e311e2da0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,7 +150,7 @@ ibig = { version = "0.3" } ics23 = { version = "0.11.0" } im = { version = "^15.1.0" } indicatif = { version = "0.16" } -jmt = { version = "0.9" } +jmt = { version = "0.10", features = ["migration"] } metrics = { version = "0.22" } metrics-tracing-context = { version = "0.15" } num-bigint = { version = "0.4" } diff --git a/crates/cnidarium/Cargo.toml b/crates/cnidarium/Cargo.toml index f4193fc4cd..9e7a9bfbbe 100644 --- a/crates/cnidarium/Cargo.toml +++ b/crates/cnidarium/Cargo.toml @@ -5,13 +5,14 @@ edition = {workspace = true} [features] migration = [] +migration-proptests = ["migration"] default = ["metrics"] rpc = ["dep:tonic", "dep:prost", "dep:serde", "dep:pbjson", "dep:ibc-proto"] [dependencies] anyhow = {workspace = true} async-trait = {workspace = true} -borsh = "0.10.3" +borsh = { version = "1.3.0" , features = ["derive", "de_strict_order"]} futures = {workspace = true} hex = {workspace = true} ibc-proto = {workspace = true, default-features = false, features = ["serde"], optional = true} @@ -37,6 +38,8 @@ tonic = {workspace = true, optional = true} tracing = {workspace = true} [dev-dependencies] -tempfile = {workspace = true} -tracing-subscriber = {workspace = true} -tokio = {workspace = true} +tempfile = { workspace = true } +tracing-subscriber = { workspace = true } +tokio = { workspace = true, features = ["full", "rt-multi-thread"] } +proptest = "1.3.1" +test-strategy = "0.3.1" diff --git a/crates/cnidarium/src/storage.rs b/crates/cnidarium/src/storage.rs index 12450e5af6..689ecd8838 100644 --- a/crates/cnidarium/src/storage.rs +++ b/crates/cnidarium/src/storage.rs @@ -323,24 +323,27 @@ impl Storage { // its own changes to the batch, and we will commit it at the end. let mut write_batch = rocksdb::WriteBatch::default(); - // Note(erwan): Here, we iterate over each substore, and spawn a task to - // commit it. Since we know that the substore keyspace is disjoint, we - // could consider rewriting this loop into a [`tokio::task::JoinSet`], - // however consider that `rocksdb::WriteBatch` is _not_ thread-safe. + // Note(erwan): Here, we spawn a commit task for each substore. + // The substore keyspaces are disjoint, so conceptually it is + // fine to rewrite it using a [`tokio::task::JoinSet`]. + // The reason this isn't done is because `rocksdb::WriteBatch` + // is _not_ thread-safe. // // This means that to spin-up N tasks, we would need to use a // single batch wrapped in a mutex, or use N batches, and find - // a way to commit to them atomically. Since that is not supported - // by RocksDB, we would have to iterate over each entry in each - // batch, and merge them together. + // a way to commit to them atomically. This isn't supported by + // RocksDB which leaves one option: to iterate over each entry + // in each batch, and merge them together. At this point, this + // is probably not worth it. // // Another option is to trade atomicity for parallelism by producing // N batches, and committing them in distinct atomic writes. This is - // dangerous because it could leave the node in an inconsistent state. + // potentially faster, but it is also more dangerous, because if one + // of the writes fails, we are left with a partially committed state. // - // Instead of doing that, we lean on the fact that the number of substores - // is small, and that the synchronization overhead of a joinset would exceed - // its benefits. + // The current implementation leans on the fact that the number of + // substores is small, and that the synchronization overhead of a joinset + // would exceed its benefits. This works well for now. for config in self.0.multistore_config.iter() { tracing::debug!(substore_prefix = ?config.prefix, "processing substore"); // If the substore is empty, we need to fetch its initialized version from the cache. @@ -376,7 +379,7 @@ impl Storage { // Commit the substore and collect the root hash let (root_hash, substore_batch) = substore_storage - .commit(changeset, write_batch, version) + .commit(changeset, write_batch, version, perform_migration) .await?; write_batch = substore_batch; @@ -417,7 +420,7 @@ impl Storage { }; let (global_root_hash, write_batch) = main_store_storage - .commit(main_store_changes, write_batch, version) + .commit(main_store_changes, write_batch, version, perform_migration) .await?; tracing::debug!( ?global_root_hash, diff --git a/crates/cnidarium/src/store/multistore.rs b/crates/cnidarium/src/store/multistore.rs index fd710f5f37..3fcb7d8604 100644 --- a/crates/cnidarium/src/store/multistore.rs +++ b/crates/cnidarium/src/store/multistore.rs @@ -90,10 +90,13 @@ impl MultistoreConfig { /// # Examples /// `prefix_a/key` -> `key` in `substore_a` /// `prefix_a` -> `prefix_a` in `main_store` - /// `preifx_a/` -> `prefix_a/` in `main_store` + /// `prefix_a/` -> `prefix_a/` in `main_store` /// `nonexistent_prefix` -> `nonexistent_prefix` in `main_store` pub fn route_key_bytes<'a>(&self, key: &'a [u8]) -> (&'a [u8], Arc) { let config = self.find_substore(key); + + // If the key is a total match for the prefix, we return the original key + // routed to the main store. This is where subtree root hashes are stored. if key == config.prefix.as_bytes() { return (key, self.main_store.clone()); } diff --git a/crates/cnidarium/src/store/substore.rs b/crates/cnidarium/src/store/substore.rs index 04fcced2e8..670878a863 100644 --- a/crates/cnidarium/src/store/substore.rs +++ b/crates/cnidarium/src/store/substore.rs @@ -360,7 +360,8 @@ impl SubstoreStorage { self, cache: Cache, mut write_batch: rocksdb::WriteBatch, - new_version: jmt::Version, + write_version: jmt::Version, + perform_migration: bool, ) -> Result<(RootHash, rocksdb::WriteBatch)> { let span = Span::current(); @@ -369,8 +370,6 @@ impl SubstoreStorage { .spawn_blocking(move || { span.in_scope(|| { let jmt = jmt::Sha256Jmt::new(&self.substore_snapshot); - - // TODO(erwan): this could be folded with sharding the changesets. let unwritten_changes: Vec<_> = cache .unwritten_changes .into_iter() @@ -394,10 +393,15 @@ impl SubstoreStorage { }; } - let (root_hash, batch) = jmt.put_value_set( - unwritten_changes.into_iter().map(|(keyhash, _key, some_value)| (keyhash, some_value)), - new_version, - )?; + // We only track the keyhash and possible values; at the time of writing, + // `rustfmt` panics on inlining the closure, so we use a helper function to skip the key. + let skip_key = |(keyhash, _key, some_value)| (keyhash, some_value); + + let (root_hash, batch) = if perform_migration { + jmt.append_value_set(unwritten_changes.into_iter().map(skip_key), write_version)? + } else { + jmt.put_value_set(unwritten_changes.into_iter().map(skip_key), write_version)? + }; self.write_node_batch(&batch.node_batch)?; tracing::trace!(?root_hash, "wrote node batch to backing store"); @@ -426,8 +430,6 @@ impl TreeWriter for SubstoreStorage { /// nodes (`DbNodeKey` -> `Node`) and the JMT values, /// (`VersionedKeyHash` -> `Option>`). fn write_node_batch(&self, node_batch: &jmt::storage::NodeBatch) -> Result<()> { - use borsh::BorshSerialize; - let node_batch = node_batch.clone(); let cf_jmt = self .substore_snapshot @@ -437,8 +439,8 @@ impl TreeWriter for SubstoreStorage { for (node_key, node) in node_batch.nodes() { let db_node_key = DbNodeKey::from(node_key.clone()); let db_node_key_bytes = db_node_key.encode()?; - let value_bytes = &node.try_to_vec()?; - tracing::trace!(?db_node_key_bytes, value_bytes = ?hex::encode(value_bytes)); + let value_bytes = borsh::to_vec(node)?; + tracing::trace!(?db_node_key_bytes, value_bytes = ?hex::encode(&value_bytes)); self.substore_snapshot .db .put_cf(cf_jmt, db_node_key_bytes, value_bytes)?; @@ -451,8 +453,8 @@ impl TreeWriter for SubstoreStorage { for ((version, key_hash), some_value) in node_batch.values() { let versioned_key = VersionedKeyHash::new(*version, *key_hash); let key_bytes = &versioned_key.encode(); - let value_bytes = &some_value.try_to_vec()?; - tracing::trace!(?key_bytes, value_bytes = ?hex::encode(value_bytes)); + let value_bytes = borsh::to_vec(some_value)?; + tracing::trace!(?key_bytes, value_bytes = ?hex::encode(&value_bytes)); self.substore_snapshot .db @@ -479,7 +481,7 @@ impl DbNodeKey { pub fn encode(&self) -> Result> { let mut bytes = Vec::new(); bytes.extend_from_slice(&self.0.version().to_be_bytes()); // encode version as big-endian - let rest = borsh::BorshSerialize::try_to_vec(&self.0)?; + let rest = borsh::to_vec(&self.0)?; bytes.extend_from_slice(&rest); Ok(bytes) } diff --git a/crates/cnidarium/tests/migration.rs b/crates/cnidarium/tests/migration.rs index 234357247c..d2d5d995c8 100644 --- a/crates/cnidarium/tests/migration.rs +++ b/crates/cnidarium/tests/migration.rs @@ -1,9 +1,11 @@ #![cfg(feature = "migration")] use cnidarium::StateDelta; -use cnidarium::StateRead; use cnidarium::StateWrite; use cnidarium::Storage; +use ibc_types::core::commitment::MerklePath; +use ibc_types::core::commitment::MerkleRoot; use jmt::RootHash; +use once_cell::sync::Lazy; use tempfile; use tokio; @@ -22,8 +24,47 @@ use tokio; * write to both the main store and any number of substores without incrementing * their version number. * + * Testing menu: + * - test_simple_migration: the most basic migration scenario where we write to the main store. + * - test_substore_migration: a migration scenario where we write to the main store and substores. + * - prop_test_substore_migration: property-based testing of the migration operation. + * + * Each test has the following pattern: + * Operation: + * Write a collection of keys, incrementing the version number at each step. + * Checks: + * - Check that the version number has incremented. + * - Check that the keys are present in the latest snapshot. + * - Check that the keys have valid proofs. + * Operation: + * Perform a migration, writing/removing a key in the main store and/or substores. + * - Check that the version number has not changed. + * - Check that the root hash for the main store and/or substores has changed. + * - Check that the migration key is present in the latest snapshot. + * - Check that the migration key has a valid proof. + * - Check that the migration key has the expected value. + * Operation: + * Write a new collection of keys, incrementing the version number at each step. + * Checks: + * - Check that the version number has incremented. + * - Check that the new keys are present in the latest snapshot. + * - Check that the new keys have valid proofs. + * - Check that the new keys have the expected values. + * Operation: + * Try to generate proofs for keys that are NOT present in the jmt + * Checks: + * - Check that no value is returned for the keys. + * - Check that the nonexistence proofs are valid. */ +/// The proof specs for the main store. +pub static MAIN_STORE_PROOF_SPEC: Lazy> = + Lazy::new(|| vec![cnidarium::ics23_spec()]); + +/// The proof specs for keys located in substores (e.g. `ibc` keys) +pub static FULL_PROOF_SPECS: Lazy> = + Lazy::new(|| vec![cnidarium::ics23_spec(), cnidarium::ics23_spec()]); + #[tokio::test] /// Test that we can commit to the main store without incrementing its version. async fn test_simple_migration() -> anyhow::Result<()> { @@ -31,52 +72,204 @@ async fn test_simple_migration() -> anyhow::Result<()> { let tmpdir = tempfile::tempdir()?; let db_path = tmpdir.into_path(); let substore_prefixes = vec![]; - let storage = Storage::load(db_path, substore_prefixes).await?; + let storage = Storage::load(db_path.clone(), substore_prefixes.clone()).await?; let mut counter = 0; - let num_writes = 10; + let num_ops = 10; - for i in 0..num_writes { + /* ************************ */ + /* write some keys */ + /* ************************ */ + let mut kvs = vec![]; + for i in 0..num_ops { + /* write some value at version `i` */ let mut delta = StateDelta::new(storage.latest_snapshot()); - let key_1 = format!("key_{i}"); - let value_1 = format!("value_{i}").as_bytes().to_vec(); - delta.put_raw(key_1.clone(), value_1.clone()); + let key = format!("key_{i}"); + let value = format!("value_{i}").as_bytes().to_vec(); + delta.put_raw(key.clone(), value.clone()); + let root_hash = storage.commit(delta).await?; - let _ = storage.commit(delta).await?; - // Check that we can read the values back out. - let snapshot = storage.latest_snapshot(); + tracing::info!(%key, ?root_hash, version = %i, "committed key-value pair"); - let retrieved_value = snapshot.get_raw(key_1.as_str()).await?.unwrap(); - assert_eq!(retrieved_value, value_1); + kvs.push((key, value)); counter += 1; } - let old_global_root = storage + assert_eq!(counter, num_ops); + counter = 0; + + // We don't _need_ to toss the storage instance, but let's be + // extra careful and make sure that we can load the storage. + storage.release().await; + let storage = Storage::load(db_path.clone(), substore_prefixes.clone()).await?; + let premigration_root = storage .latest_snapshot() .root_hash() .await .expect("infaillible"); + + for (i, (key, value)) in kvs.clone().into_iter().enumerate() { + let snapshot = storage.latest_snapshot(); + let (some_value, proof) = snapshot.get_with_proof(key.as_bytes().to_vec()).await?; + let retrieved_value = some_value.expect("key is found in the latest snapshot"); + assert_eq!(retrieved_value, value); + + let merkle_path = MerklePath { + key_path: vec![key], + }; + let merkle_root = MerkleRoot { + hash: premigration_root.0.to_vec(), + }; + + proof + .verify_membership( + &MAIN_STORE_PROOF_SPEC, + merkle_root, + merkle_path, + retrieved_value, + 0, + ) + .map_err(|e| tracing::error!(?e, key_index = ?i, "proof verification failed")) + .expect("membership proof verifies"); + + counter += 1; + } + + assert_eq!(counter, num_ops); + counter = 0; + let old_version = storage.latest_version(); - assert_eq!(old_version, counter - 1); + assert_eq!(old_version, num_ops - 1); /* ********************* */ /* perform the migration */ /* ********************* */ let mut delta = StateDelta::new(storage.latest_snapshot()); - let key_root_2 = "migration".to_string(); - let value_root_2 = "migration data".as_bytes().to_vec(); - delta.put_raw(key_root_2, value_root_2); - let new_global_root = storage.commit_in_place(delta).await?; + let migration_key = "banana".to_string(); + let migration_value = "a good fruit".as_bytes().to_vec(); + delta.put_raw(migration_key.clone(), migration_value.clone()); + let postmigration_root = storage.commit_in_place(delta).await?; + + // We have to reload the storage instance to get the latest snapshot. + storage.release().await; + let storage = Storage::load(db_path, substore_prefixes).await?; + let new_version = storage.latest_version(); assert_ne!( - old_global_root, new_global_root, - "migration did not effect the root hash" + premigration_root, postmigration_root, + "migration should change the root hash" + ); + assert_eq!( + old_version, new_version, + "the post-migration version number should not change" ); - assert_eq!(old_version, new_version, "the version number has changed!"); + /* ************************ */ + /* check the migration */ + /* ************************ */ + let (some_value, proof) = storage + .latest_snapshot() + .get_with_proof(migration_key.as_bytes().to_vec()) + .await?; + let retrieved_value = some_value.expect("migration key is found in the latest snapshot"); + assert_eq!(retrieved_value, migration_value); + + let merkle_path = MerklePath { + key_path: vec![migration_key], + }; + let merkle_root = MerkleRoot { + hash: postmigration_root.0.to_vec(), + }; + + proof + .verify_membership( + &MAIN_STORE_PROOF_SPEC, + merkle_root, + merkle_path, + retrieved_value, + 0, + ) + .map_err(|e| tracing::error!("proof verification failed: {:?}", e)) + .expect("membership proof verifies"); + + /* ************************ */ + /* write new keys */ + /* ************************ */ + for i in num_ops..num_ops * 2 { + /* write some value at version `i` */ + let mut delta = StateDelta::new(storage.latest_snapshot()); + let key = format!("key_{i}"); + let value = format!("value_{i}").as_bytes().to_vec(); + delta.put_raw(key.clone(), value.clone()); + let root_hash = storage.commit(delta).await?; + + tracing::info!(%key, ?root_hash, version = %i, "committed key-value pair"); + + kvs.push((key, value)); + counter += 1; + } + + assert_eq!(counter, num_ops); + counter = 0; + + let final_root = storage + .latest_snapshot() + .root_hash() + .await + .expect("infaillible"); + + for (i, (key, value)) in kvs.clone().into_iter().enumerate() { + let snapshot = storage.latest_snapshot(); + let (some_value, proof) = snapshot.get_with_proof(key.as_bytes().to_vec()).await?; + let retrieved_value = some_value.expect("key is found in the latest snapshot"); + assert_eq!(retrieved_value, value); + + let merkle_path = MerklePath { + key_path: vec![key], + }; + let merkle_root = MerkleRoot { + hash: final_root.0.to_vec(), + }; + + proof + .verify_membership( + &MAIN_STORE_PROOF_SPEC, + merkle_root, + merkle_path, + retrieved_value, + 0, + ) + .map_err(|e| tracing::error!(?e, key_index = ?i, "proof verification failed")) + .expect("membership proof verifies"); + + counter += 1; + } + + assert_eq!(counter, num_ops * 2); - assert_eq!(counter, num_writes); + /* ****************************** */ + /* read nonexistent keys */ + /* ****************************** */ + let final_snapshot = storage.latest_snapshot(); + let final_root = final_snapshot.root_hash().await.expect("infaillible"); + + let key = format!("nonexistent_key"); + let (some_value, proof) = final_snapshot + .get_with_proof(key.as_bytes().to_vec()) + .await?; + assert!(some_value.is_none()); + let merkle_path = MerklePath { + key_path: vec![key], + }; + let merkle_root = MerkleRoot { + hash: final_root.0.to_vec(), + }; + + proof + .verify_non_membership(&MAIN_STORE_PROOF_SPEC, merkle_root, merkle_path) + .map_err(|e| tracing::error!("proof verification failed: {:?}", e)) + .expect("nonmembership proof verifies"); Ok(()) } @@ -87,42 +280,84 @@ async fn test_substore_migration() -> anyhow::Result<()> { let _ = tracing_subscriber::fmt::try_init(); let tmpdir = tempfile::tempdir()?; let db_path = tmpdir.into_path(); - let substore_prefixes = vec!["ibc/".to_string(), "dex/".to_string(), "misc/".to_string()]; - let all_substores = vec!["ibc/".to_string(), "dex/".to_string(), "".to_string()]; + let substore_prefixes = vec!["ibc".to_string(), "dex".to_string(), "misc".to_string()]; let storage = Storage::load(db_path.clone(), substore_prefixes.clone()).await?; let mut counter = 0; - let num_writes = 10; + let num_ops_per_substore = 10; - // Write to every substore, multiple times and check that we can read the values back out. - for i in 0..num_writes { + let mut kvs = vec![]; + + /* ************************ */ + /* write some keys */ + /* in every substore */ + /* ************************ */ + for i in 0..num_ops_per_substore { let mut delta = StateDelta::new(storage.latest_snapshot()); - let mut keys: Vec = vec![]; - let mut values: Vec> = vec![]; - for substore in all_substores.iter() { + for substore in substore_prefixes.iter() { let key = format!("{substore}/key_{i}"); let value = format!("{substore}value_{i}").as_bytes().to_vec(); + kvs.push((key.clone(), value.clone())); tracing::debug!(?key, "initializing substore {substore} with key-value pair"); delta.put_raw(key.clone(), value.clone()); - keys.push(key); - values.push(value); } - let _ = storage.commit(delta).await?; + let root_hash = storage.commit(delta).await?; + tracing::info!(?root_hash, version = %i, "committed key-value pair"); + counter += 1; + } + let num_versions_pre_migration = counter; + assert_eq!(counter, num_ops_per_substore); + counter = 0; + + // We don't _need_ to toss the storage instance, but let's be + // extra careful and make sure that things work if we reload it. + storage.release().await; + let storage = Storage::load(db_path.clone(), substore_prefixes.clone()).await?; + + let premigration_root = storage + .latest_snapshot() + .root_hash() + .await + .expect("infaillible"); + + for (i, (key, value)) in kvs.clone().into_iter().enumerate() { + tracing::debug!(?key, "checking key-value pair"); let snapshot = storage.latest_snapshot(); + let (some_value, proof) = snapshot.get_with_proof(key.as_bytes().to_vec()).await?; + let retrieved_value = some_value.expect("key is found in the latest snapshot"); + assert_eq!(retrieved_value, value); - for (key, value) in keys.iter().zip(values.iter()) { - let retrieved_value = snapshot.get_raw(key.as_str()).await?.unwrap(); - assert_eq!(retrieved_value, *value); - } + // We split the key into its substore prefix and the key itself. + let merkle_path = MerklePath { + key_path: key.split('/').map(|s| s.to_string()).collect(), + }; + let merkle_root = MerkleRoot { + hash: premigration_root.0.to_vec(), + }; + + proof + .verify_membership( + &FULL_PROOF_SPECS, + merkle_root, + merkle_path, + retrieved_value, + 0, + ) + .map_err(|e| tracing::error!(?e, key_index = ?i, "proof verification failed")) + .expect("membership proof verifies"); counter += 1; } - assert_eq!(counter, num_writes); + + assert_eq!( + counter, + substore_prefixes.len() as u64 * num_ops_per_substore + ); let premigration_snapshot = storage.latest_snapshot(); let mut old_root_hashes: Vec = vec![]; - for substore in all_substores.iter() { + for substore in substore_prefixes.iter() { let root_hash = premigration_snapshot .prefix_root_hash(substore.as_str()) .await @@ -130,7 +365,7 @@ async fn test_substore_migration() -> anyhow::Result<()> { old_root_hashes.push(root_hash); } - let old_substore_versions: Vec = all_substores + let old_substore_versions: Vec = substore_prefixes .clone() .into_iter() .map(|prefix| { @@ -142,7 +377,11 @@ async fn test_substore_migration() -> anyhow::Result<()> { .collect(); let old_version = storage.latest_version(); - assert_eq!(old_version, counter - 1); // -1 because we start at u64::MAX + assert_eq!(old_version, num_versions_pre_migration - 1); // -1 because we start at u64::MAX + let premigration_root_hash = premigration_snapshot + .root_hash() + .await + .expect("infaillible"); drop(premigration_snapshot); /* ******************************* */ @@ -150,37 +389,57 @@ async fn test_substore_migration() -> anyhow::Result<()> { /* (write a key in every substore) */ /* ******************************* */ let mut delta = StateDelta::new(storage.latest_snapshot()); + let mut migration_kvs = vec![]; // Start by writing a key in every substore, including the main store. - for substore in all_substores.iter() { - let key = format!("{substore}/migration", substore = substore); - let value = format!("{substore}migration data", substore = substore) + for substore in substore_prefixes.iter() { + let key = format!("{substore}/banana", substore = substore); + let value = format!("{substore}", substore = substore) .as_bytes() .to_vec(); tracing::debug!(?key, "migration: writing to substore {substore}"); - delta.put_raw(key, value); + delta.put_raw(key.clone(), value.clone()); + migration_kvs.push((key, value)); } // Commit the migration. let _ = storage.commit_in_place(delta).await?; - // Note(erwan): when we perform a commit in-place, we do not update the - // snapshot cache. This means that querying `Storage::latest_snapshot()` - // will return a now-stale view of the state. + /* ************************ */ + /* check the migration */ + /* ************************ */ + // Overview: We just wrote a key in every substore. Now we want to perform increasingly + // complex checks to ensure that the migration was successful. + // 1. Check that every root hash has changed + // 2. Check that no version number has changed + // 3. Check that we can read the migration key from every substore + // 4. Check that the migration key has a valid proof + // 5. Check that we can read every other key from every substore + // 6. Check that every other key has a valid proof + + // We reload storage so that we can access the latest snapshot. + // The snapshot cache is not updated when we commit in place. storage.release().await; - let storage = Storage::load(db_path, substore_prefixes).await?; + let storage = Storage::load(db_path.clone(), substore_prefixes.clone()).await?; let postmigration_snapshot = storage.latest_snapshot(); let new_version = storage.latest_version(); assert_eq!( old_version, new_version, - "the global version number has changed!" + "the global version should not change" ); + let postmigration_root_hash = postmigration_snapshot + .root_hash() + .await + .expect("infaillible"); + + assert_ne!(premigration_root_hash, postmigration_root_hash); + // Check that the root hash for every substore has changed. let mut new_root_hashes: Vec = vec![]; - for substore in all_substores.iter() { + for substore in substore_prefixes.iter() { let root_hash = postmigration_snapshot .prefix_root_hash(substore.as_str()) .await @@ -191,16 +450,25 @@ async fn test_substore_migration() -> anyhow::Result<()> { old_root_hashes .iter() .zip(new_root_hashes.iter()) - .zip(all_substores.iter()) + .zip(substore_prefixes.iter()) .for_each(|((old, new), substore)| { assert_ne!( old, new, "migration did not effect the root hash for substore {substore}", ); + let substore_version = postmigration_snapshot + .prefix_version(substore.as_str()) + .expect("prefix exists") + .unwrap(); + assert_eq!( + substore_version, + num_ops_per_substore - 1, + "substore version should not change" + ); }); // Check that the version number for every substore has NOT changed. - let new_substore_versions: Vec = all_substores + let new_substore_versions: Vec = substore_prefixes .clone() .into_iter() .map(|prefix| { @@ -214,12 +482,709 @@ async fn test_substore_migration() -> anyhow::Result<()> { old_substore_versions .iter() .zip(new_substore_versions.iter()) - .zip(all_substores.iter()) + .zip(substore_prefixes.iter()) .for_each(|((old, new), substore)| { assert_eq!( old, new, "the version number for substore {substore} has changed!", ); }); + + // Check that the migration key is present in the latest snapshot. + for (migration_key, migration_value) in migration_kvs.clone().into_iter() { + let (some_value, proof) = postmigration_snapshot + .get_with_proof(migration_key.as_bytes().to_vec()) + .await?; + let retrieved_value = some_value.expect("migration key is found in the latest snapshot"); + assert_eq!(retrieved_value, migration_value); + + let merkle_path = MerklePath { + key_path: migration_key.split('/').map(|s| s.to_string()).collect(), + }; + let merkle_root = MerkleRoot { + hash: postmigration_root_hash.0.to_vec(), + }; + + proof + .verify_membership( + &FULL_PROOF_SPECS, + merkle_root, + merkle_path, + retrieved_value, + 0, + ) + .map_err(|e| tracing::error!("proof verification failed: {:?}", e)) + .expect("membership proof verifies"); + } + + // Check that every other key is still present in the latest snapshot. + for (key, value) in kvs.clone().into_iter() { + let (some_value, proof) = postmigration_snapshot + .get_with_proof(key.as_bytes().to_vec()) + .await?; + let retrieved_value = some_value.expect("key is found in the latest snapshot"); + assert_eq!(retrieved_value, value); + + let merkle_path = MerklePath { + key_path: key.split('/').map(|s| s.to_string()).collect(), + }; + let merkle_root = MerkleRoot { + hash: postmigration_root_hash.0.to_vec(), + }; + + proof + .verify_membership( + &FULL_PROOF_SPECS, + merkle_root, + merkle_path, + retrieved_value, + 0, + ) + .map_err(|e| tracing::error!("proof verification failed: {:?}", e)) + .expect("membership proof verifies"); + } + + /* ************************ */ + /* write some keys */ + /* in every substore */ + /* ... again ... */ + /* ************************ */ + counter = 0; + for i in 0..num_ops_per_substore { + let mut delta = StateDelta::new(storage.latest_snapshot()); + for substore in substore_prefixes.iter() { + let key = format!("{substore}/key_{i}"); + let value = format!("{substore}value_{i}").as_bytes().to_vec(); + kvs.push((key.clone(), value.clone())); + tracing::debug!(?key, "initializing substore {substore} with key-value pair"); + delta.put_raw(key.clone(), value.clone()); + } + + let root_hash = storage.commit(delta).await?; + tracing::info!(?root_hash, version = %i, "committed key-value pair"); + counter += 1; + } + assert_eq!(counter, num_ops_per_substore); + counter = 0; + + let final_root = storage + .latest_snapshot() + .root_hash() + .await + .expect("infaillible"); + + for (i, (key, value)) in kvs.clone().into_iter().enumerate() { + tracing::debug!(?key, "checking key-value pair"); + let snapshot = storage.latest_snapshot(); + let (some_value, proof) = snapshot.get_with_proof(key.as_bytes().to_vec()).await?; + let retrieved_value = some_value.expect("key is found in the latest snapshot"); + assert_eq!(retrieved_value, value); + + // We split the key into its substore prefix and the key itself. + let merkle_path = MerklePath { + key_path: key.split('/').map(|s| s.to_string()).collect(), + }; + let merkle_root = MerkleRoot { + hash: final_root.0.to_vec(), + }; + + proof + .verify_membership( + &FULL_PROOF_SPECS, + merkle_root, + merkle_path, + retrieved_value, + 0, + ) + .map_err(|e| tracing::error!(?e, key_index = ?i, "proof verification failed")) + .expect("membership proof verifies"); + + counter += 1; + } + + assert_eq!( + counter, + // For each substore, we wrote `num_ops_per_substore` keys twice. + substore_prefixes.len() as u64 * num_ops_per_substore * 2 + ); + + /* ****************************** */ + /* read nonexistent keys */ + /* ****************************** */ + for (idx, substore) in substore_prefixes.iter().enumerate() { + let key = format!("{substore}/nonexistent_key_{idx}"); + let (some_value, proof) = postmigration_snapshot + .get_with_proof(key.as_bytes().to_vec()) + .await?; + assert!(some_value.is_none()); + let merkle_path = MerklePath { + key_path: key.split('/').map(|s| s.to_string()).collect(), + }; + let merkle_root = MerkleRoot { + hash: final_root.0.to_vec(), + }; + + proof + .verify_non_membership(&FULL_PROOF_SPECS, merkle_root, merkle_path) + .map_err(|e| tracing::error!("proof verification failed: {:?}", e)) + .expect("nonmembership proof verifies"); + } + Ok(()) } + +#[cfg(feature = "migration-proptests")] +mod proptests { + use proptest::{ + arbitrary::any, + prelude::prop, + prop_assert, prop_assert_eq, prop_assert_ne, prop_oneof, + strategy::{BoxedStrategy, Just, Strategy}, + test_runner::{FileFailurePersistence, TestCaseError}, + }; + use sha2::Sha256; + use std::{ + collections::BTreeMap, + fmt::{Debug, Display, Formatter}, + path::PathBuf, + }; + use test_strategy::proptest; + + use cnidarium::{StateDelta, StateRead, StateWrite as _, Storage}; + use ibc_types::core::commitment::{MerklePath, MerkleRoot}; + + use crate::{FULL_PROOF_SPECS, MAIN_STORE_PROOF_SPEC}; + + struct ReferenceStore { + final_kv: BTreeMap>>, + } + + impl ReferenceStore { + fn new() -> Self { + Self { + final_kv: BTreeMap::new(), + } + } + + fn execute(&mut self, op: Operation) { + match op { + Operation::Insert(key, value) => { + if key.path() == "" { + panic!("empty key"); + } + self.final_kv.insert(key, Some(value)); + } + Operation::Delete(key) => { + self.final_kv.insert(key, None); + } + } + } + } + + fn prefix_list() -> Vec { + vec![ + "".to_string(), + "dex".to_string(), + "ibc".to_string(), + "misc".to_string(), + "staking".to_string(), + ] + } + + fn substore_list() -> Vec { + vec![ + "dex".to_string(), + "ibc".to_string(), + "misc".to_string(), + "staking".to_string(), + ] + } + + fn char_except_slash() -> impl Strategy { + any::().prop_filter("Exclude '/'", |c| *c != '/') + } + + fn valid_key_strategy() -> impl Strategy { + ( + char_except_slash(), + proptest::collection::vec(any::(), 0..=999), + ) + .prop_map(|(first_char, mut vec)| { + vec.insert(0, first_char); + vec.into_iter().collect() + }) + } + + fn storage_key_strategy() -> BoxedStrategy { + let prefixes = prefix_list(); + let substore_strategies: Vec> = + prefixes.into_iter().map(|s| Just(s).boxed()).collect(); + + let substore_strategy = prop::strategy::Union::new_weighted( + substore_strategies.into_iter().map(|s| (1, s)).collect(), + ); + + let key_strategy = valid_key_strategy(); + + (substore_strategy, key_strategy) + .prop_map(|(substore, key)| StorageKey::new(substore, key)) + .boxed() + } + + fn value_strategy() -> impl Strategy> { + // Generate a random byte array of length 1..10 + // The values don't actually matter for the tests. + prop::collection::vec(any::(), 1..10) + } + + fn operation_strategy() -> impl Strategy { + let insert_strategy = (storage_key_strategy(), value_strategy()) + .prop_map(|(key, value)| Operation::Insert(key, value)); + + let delete_strategy = storage_key_strategy().prop_map(Operation::Delete); + + prop_oneof![insert_strategy, delete_strategy,] + } + + fn insert_strategy() -> impl Strategy { + let insert_strategy = (storage_key_strategy(), value_strategy()) + .prop_map(|(key, value)| Operation::Insert(key, value)); + + prop_oneof![insert_strategy] + } + + fn insert_ops_strategy() -> impl Strategy> { + prop::collection::vec(insert_strategy(), 0..10000) + } + + fn operations_strategy() -> impl Strategy> { + prop::collection::vec(operation_strategy(), 0..10000) + } + + fn execute_transcript( + phase: &str, + reference_store: &mut ReferenceStore, + delta: &mut StateDelta, + transcript: Vec, + ) { + for op in transcript { + reference_store.execute(op.clone()); + let storage_key = op.key(); + let key_hash = storage_key.hash(); + let key_path = storage_key.path(); + + tracing::debug!( + prefix = storage_key.prefix(), + key = storage_key.truncated_key(), + ?key_hash, + ?op, + ?phase, + ); + + match op { + Operation::Insert(_key, value) => { + delta.put_raw(key_path, value); + } + Operation::Delete(_key) => { + delta.delete(key_path); + } + } + } + } + + async fn check_proofs( + state: cnidarium::Snapshot, + reference_store: &ReferenceStore, + root_hash: jmt::RootHash, + phase: &str, + ) -> Result<(), TestCaseError> { + for (storage_key, reference_value) in reference_store.final_kv.iter() { + let key_hash = storage_key.hash(); + let key_path = storage_key.path(); + + tracing::debug!( + prefix = storage_key.prefix(), + key = storage_key.truncated_key(), + ?key_hash, + ?phase, + "checking proofs" + ); + let result_proof = state + .get_with_proof(storage_key.encode_path()) + .await + .map_err(|e| tracing::error!(?e, "get_with_proof failed")); + + prop_assert!(result_proof.is_ok(), "can get with proof"); + + let (retrieved_value, proof) = result_proof.expect("can get with proof"); + prop_assert_eq!(&retrieved_value, reference_value); + + let merkle_path = storage_key.merkle_path(); + let merkle_root = MerkleRoot { + hash: root_hash.0.to_vec(), + }; + let specs = storage_key.proof_spec(); + let key_hash = jmt::KeyHash::with::(&key_path); + + tracing::debug!( + prefix = storage_key.prefix(), + num_proofs = proof.proofs.len(), + spec_len = specs.len(), + ?key_hash, + truncated_key = storage_key.truncated_key(), + is_existence = reference_value.is_some(), + "proof verification" + ); + + let proof_verifies = if let Some(value) = retrieved_value.clone() { + proof + .verify_membership(&specs, merkle_root, merkle_path, value, 0) + .map_err(|e| tracing::error!(?e, "existence proof failed")) + } else { + proof + .verify_non_membership(&specs, merkle_root, merkle_path) + .map_err(|e| tracing::error!(?e, "nonexistence proof failed")) + }; + + prop_assert!(proof_verifies.is_ok()); + } + Ok(()) + } + + /// Implements a standard migration test which consists of three phases: + /// - premigration: write and delete keys, check ex/nex proofs + /// - migration: write and delete keys using a migration commit, check ex/nex proofs + /// - postmigration: write new keys, check ex/nex proofs + async fn standard_migration_test( + db_path: PathBuf, + premigration_transcript: Vec, + migration_transcript: Vec, + postmigration_transcript: Vec, + ) -> Result<(), TestCaseError> { + let substore_prefixes = substore_list(); + + let premigration_len = premigration_transcript.len(); + let migration_len = migration_transcript.len(); + let postmigration_len = postmigration_transcript.len(); + let total_ops = premigration_len + migration_len + postmigration_len; + + tracing::info!( + premigration_len, + migration_len, + postmigration_len, + total_ops, + "starting test" + ); + + let storage = Storage::load(db_path.clone(), substore_prefixes.clone()) + .await + .expect("Failed to load storage"); + // `ReferenceStore` is an in-memory store that tracks the latest value for each key + // To do this, the store execute each operation in the transcript and tracks the final value. + // It serves as a source of truth to compare the storage against. + let mut reference_store = ReferenceStore::new(); + + // Premigration: write and delete keys + let mut premigration_delta = StateDelta::new(storage.latest_snapshot()); + + execute_transcript( + "premigration", + &mut reference_store, + &mut premigration_delta, + premigration_transcript, + ); + + let premigration_root_hash = storage + .commit(premigration_delta) + .await + .expect("can commit premigration"); + let premigration_version = storage.latest_version(); + prop_assert_eq!(premigration_version, 0, "premigration version should be 0"); + + tracing::info!( + ?premigration_root_hash, + premigration_len, + "premigration operations have been committed" + ); + + let premigration_snapshot = storage.latest_snapshot(); + let _ = check_proofs( + premigration_snapshot, + &reference_store, + premigration_root_hash, + "premigration", + ) + .await?; + + // Migration: write and delete keys + let mut migration_delta = StateDelta::new(storage.latest_snapshot()); + execute_transcript( + "migration", + &mut reference_store, + &mut migration_delta, + migration_transcript, + ); + + let migration_root_hash = storage + .commit_in_place(migration_delta) + .await + .expect("can commit migration"); + let migration_version = storage.latest_version(); + prop_assert_eq!(migration_version, 0, "migration version should be 0"); + prop_assert_ne!( + migration_root_hash, + premigration_root_hash, + "migration root hash should be different than the premigration root hash" + ); + + storage.release().await; + let storage = Storage::load(db_path.clone(), substore_prefixes.clone()) + .await + .expect("can reload storage"); + + tracing::info!( + ?migration_root_hash, + migration_len, + "migration operations have been committed" + ); + + let migration_snapshot = storage.latest_snapshot(); + + let _ = check_proofs( + migration_snapshot, + &reference_store, + migration_root_hash, + "migration", + ) + .await?; + + // We toss the storage instance and reload it. + storage.release().await; + let storage = Storage::load(db_path.clone(), substore_prefixes.clone()) + .await + .expect("can reload storage"); + + // Post-migration: write new keys! + let mut postmigration_delta = StateDelta::new(storage.latest_snapshot()); + execute_transcript( + "postmigration", + &mut reference_store, + &mut postmigration_delta, + postmigration_transcript, + ); + + let postmigration_root_hash = storage + .commit(postmigration_delta) + .await + .expect("can commit postmigration"); + tracing::info!( + ?postmigration_root_hash, + num_ops = postmigration_len, + "postmigration operations have been committed" + ); + + let postmigration_version = storage.latest_version(); + prop_assert_eq!( + postmigration_version, + 1, + "postmigration version should be 1" + ); + prop_assert_ne!( + migration_root_hash, + postmigration_root_hash, + "postmigration root hash should be different than the migration root hash" + ); + + let post_migration_snapshot = storage.latest_snapshot(); + + let _ = check_proofs( + post_migration_snapshot, + &reference_store, + postmigration_root_hash, + "postmigration", + ) + .await?; + Ok(()) + } + + #[proptest(async = "tokio", cases = 100, failure_persistence = Some(Box::new(FileFailurePersistence::WithSource("regressions"))))] + async fn test_migration_substores( + #[strategy(operations_strategy())] premigration_transcript: Vec, + #[strategy(operations_strategy())] migration_transcript: Vec, + #[strategy(operations_strategy())] postmigration_transcript: Vec, + #[strategy(insert_ops_strategy())] nonexistence_keys: Vec, + ) { + let _ = tracing_subscriber::fmt::try_init(); + + let tmpdir = tempfile::tempdir().expect("Failed to create a temp dir"); + let db_path = tmpdir.into_path(); + let _ = standard_migration_test( + db_path.clone(), + premigration_transcript, + migration_transcript, + postmigration_transcript, + ) + .await; + + let storage = Storage::load(db_path.clone(), substore_list()) + .await + .expect("can reload storage"); + + let postmigration_root_hash = storage + .latest_snapshot() + .root_hash() + .await + .expect("infaillible"); + + // Check random keys that should not exist + for op in nonexistence_keys { + let storage_key = op.key(); + let key_hash = storage_key.hash(); + let key_path = storage_key.path(); + + tracing::debug!( + prefix = storage_key.prefix(), + key = &storage_key.truncated_key(), + ?key_hash, + ?op, + "nex: checking proofs" + ); + + let result_proof = storage + .latest_snapshot() + .get_with_proof(storage_key.encode_path()) + .await + .map_err(|e| tracing::error!(?e, "nex: get_with_proof failed")); + + prop_assert!(result_proof.is_ok(), "can get with proof"); + + let (retrieved_value, proof) = result_proof.expect("can get with proof"); + prop_assert!(retrieved_value.is_none(), "key should not exist"); + + let merkle_path = storage_key.merkle_path(); + let merkle_root = MerkleRoot { + hash: postmigration_root_hash.0.to_vec(), + }; + let specs = storage_key.proof_spec(); + let key_hash = jmt::KeyHash::with::(&key_path); + + tracing::debug!( + prefix = storage_key.prefix(), + num_proofs = proof.proofs.len(), + spec_len = specs.len(), + ?key_hash, + truncated_key = &storage_key.truncated_key(), + "nex: proof verification" + ); + + let proof_verifies = proof + .verify_non_membership(&specs, merkle_root, merkle_path) + .map_err(|e| tracing::error!(?e, "nonexistence: nonexistence proof failed")); + + prop_assert!(proof_verifies.is_ok()); + } + } + + #[derive(Clone)] + enum Operation { + Insert(StorageKey, Vec), + Delete(StorageKey), + } + + impl Operation { + fn key(&self) -> &StorageKey { + match self { + Operation::Insert(key, _) => key, + Operation::Delete(key) => key, + } + } + } + + impl Debug for Operation { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Operation::Insert(_, _) => write!(f, "Insert"), + Operation::Delete(_) => write!(f, "Delete"), + } + } + } + + #[derive(PartialEq, Eq, Clone, PartialOrd, Ord)] + struct StorageKey { + prefix: String, + key: String, + full_path: String, + } + + impl Display for StorageKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.path()) + } + } + + impl Debug for StorageKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.full_path) + } + } + + impl StorageKey { + fn new(prefix: String, key: String) -> Self { + let full_path = if prefix.is_empty() { + key.clone() + } else { + format!("{}/{}", prefix, key) + }; + + Self { + prefix, + key, + full_path, + } + } + + fn truncated_key(&self) -> String { + self.key.chars().take(5).collect() + } + + fn hash(&self) -> jmt::KeyHash { + jmt::KeyHash::with::(&self.full_path) + } + + fn encode_path(&self) -> Vec { + self.path().as_bytes().to_vec() + } + + fn prefix(&self) -> &String { + &self.prefix + } + + fn is_main_store(&self) -> bool { + self.prefix == "" + } + + fn path(&self) -> String { + self.full_path.clone() + } + + fn merkle_path(&self) -> MerklePath { + if self.is_main_store() { + return MerklePath { + key_path: vec![self.key.clone()], + }; + } else { + MerklePath { + key_path: vec![self.prefix.clone(), self.key.clone()], + } + } + } + + fn proof_spec(&self) -> Vec { + if self.is_main_store() { + MAIN_STORE_PROOF_SPEC.clone() + } else { + FULL_PROOF_SPECS.clone() + } + } + } +} From c59ed532203015fed8434b5a61ffefcb66155a41 Mon Sep 17 00:00:00 2001 From: Conor Schaefer Date: Tue, 19 Mar 2024 15:50:25 -0700 Subject: [PATCH 005/168] feat(pd): support archives for migrate and join Enables opt-in archive generation when performing: * pd export * pd migrate The goal is to provide a standardized bottling-up of pd state, specifically the rocksdb directory. In the context of upgrades, the `pd migrate` functionality is what's important: we want the archived dir to contain both rocksdb data and the modified genesis file and updated validator state. Accordingly, `pd testnet join` is modified to support an optional archive URL. If set, the remote tar.gz archive will be downloaded and extracted, clobbering the cometbft config files that were fetched. A remote bootstrap node is still contacted, to learn about peers, otherwise the newly created node wouldn't be able to talk to the network. --- Cargo.lock | 51 +++++++++++++++++ crates/bin/pd/Cargo.toml | 4 +- crates/bin/pd/src/cli.rs | 26 +++++++-- crates/bin/pd/src/main.rs | 91 ++++++++++++++++++++++--------- crates/bin/pd/src/migrate.rs | 48 +++++++++++++--- crates/bin/pd/src/testnet/join.rs | 79 +++++++++++++++++++++++++++ 6 files changed, 261 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7b21112819..47f8156a7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2350,6 +2350,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.52.0", +] + [[package]] name = "fixed-hash" version = "0.8.0" @@ -4536,6 +4548,7 @@ dependencies = [ "decaf377-rdsa", "directories", "ed25519-consensus", + "flate2", "fs_extra", "futures", "hex", @@ -4584,6 +4597,7 @@ dependencies = [ "serde_json", "serde_with", "sha2 0.10.8", + "tar", "tempfile", "tendermint", "tendermint-config", @@ -6638,10 +6652,12 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls 0.24.1", + "tokio-util 0.7.10", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "winreg", ] @@ -7729,6 +7745,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.10.0" @@ -8717,6 +8744,19 @@ version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.68" @@ -8992,6 +9032,17 @@ dependencies = [ "time 0.3.34", ] +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys 0.4.13", + "rustix 0.38.31", +] + [[package]] name = "yasna" version = "0.5.2" diff --git a/crates/bin/pd/Cargo.toml b/crates/bin/pd/Cargo.toml index 1ff147f4cb..9951853bb1 100644 --- a/crates/bin/pd/Cargo.toml +++ b/crates/bin/pd/Cargo.toml @@ -50,6 +50,7 @@ decaf377 = { workspace = true, features = ["parallel"], decaf377-rdsa = { workspace = true } directories = { workspace = true } ed25519-consensus = { workspace = true } +flate2 = "1.0.28" fs_extra = "1.3.0" futures = { workspace = true } hex = { workspace = true } @@ -91,12 +92,13 @@ rand = { workspace = true } rand_chacha = { workspace = true } rand_core = { workspace = true, features = ["getrandom"] } regex = { workspace = true } -reqwest = { version = "0.11", features = ["json"] } +reqwest = { version = "0.11", features = ["json", "stream"] } rocksdb = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } serde_with = { workspace = true, features = ["hex"] } sha2 = { workspace = true } +tar = "0.4.40" tempfile = { workspace = true } tendermint = { workspace = true } tendermint-config = { workspace = true } diff --git a/crates/bin/pd/src/cli.rs b/crates/bin/pd/src/cli.rs index 3cb1f54fbf..72977b9542 100644 --- a/crates/bin/pd/src/cli.rs +++ b/crates/bin/pd/src/cli.rs @@ -113,9 +113,13 @@ pub enum RootCommand { /// The home directory of the full node. #[clap(long, env = "PENUMBRA_PD_HOME", display_order = 100)] home: PathBuf, - /// The directory that the exported state will be written to. + /// The directory where the exported node state will be written. + #[clap(long, display_order = 200, alias = "export-path")] + export_directory: PathBuf, + /// An optional filepath for a compressed archive containing the exported + /// node state, e.g. ~/pd-backup.tar.gz. #[clap(long, display_order = 200)] - export_path: PathBuf, + export_archive: Option, /// Whether to prune the JMT tree. #[clap(long, display_order = 300)] prune: bool, @@ -123,13 +127,18 @@ pub enum RootCommand { /// Run a migration on the exported storage state of the full node, /// and create a genesis file. Migrate { - /// The directory containing exported state to which the upgrade will be applied. - #[clap(long, display_order = 200)] - target_dir: PathBuf, + /// The directory containing exported state, created via `pd export`, to be modified + /// in-place. This should be a pd home directory, with a subdirectory called "rocksdb". + #[clap(long, display_order = 200, alias = "target-dir")] + target_directory: PathBuf, #[clap(long, display_order = 300)] /// Timestamp of the genesis file in RFC3339 format. If unset, defaults to the current time, /// unless the migration logic overrides it. genesis_start: Option, + /// An optional filepath for a compressed archive containing the migrated node state, + /// e.g. ~/pd-state-post-upgrade.tar.gz. + #[clap(long, display_order = 400)] + migrate_archive: Option, }, } @@ -197,6 +206,13 @@ pub enum TestnetCommand { default_value = "https://rpc.testnet.penumbra.zone" )] node: Url, + + /// Optional URL of archived node state in .tar.gz format. The archive will be + /// downloaded and extracted locally, allowing the node to join a network at a block height + /// higher than 0. + #[clap(long)] + archive_url: Option, + /// Human-readable name to identify node on network // Default: 'node-#' #[clap(long, env = "PENUMBRA_PD_TM_MONIKER")] diff --git a/crates/bin/pd/src/main.rs b/crates/bin/pd/src/main.rs index 29d2ecbe7e..b4bd928966 100644 --- a/crates/bin/pd/src/main.rs +++ b/crates/bin/pd/src/main.rs @@ -255,6 +255,7 @@ async fn main() -> anyhow::Result<()> { tn_cmd: TestnetCommand::Join { node, + archive_url, moniker, external_address, tendermint_rpc_bind, @@ -290,7 +291,7 @@ async fn main() -> anyhow::Result<()> { // Join the target testnet, looking up network info and writing // local configs for pd and tendermint. testnet_join( - output_dir, + output_dir.clone(), node, &node_name, external_address, @@ -298,6 +299,11 @@ async fn main() -> anyhow::Result<()> { tendermint_p2p_bind, ) .await?; + + // Download and extract archive URL, if set. + if let Some(archive_url) = archive_url { + pd::testnet::join::unpack_state_archive(archive_url, output_dir).await?; + } } RootCommand::Testnet { @@ -379,44 +385,79 @@ async fn main() -> anyhow::Result<()> { t.write_configs()?; } RootCommand::Export { - mut home, - mut export_path, + home, + export_directory, + export_archive, prune, } => { use fs_extra; - tracing::info!("exporting state to {}", export_path.display()); + // Export state as directory. + let src_rocksdb_dir = home.join("rocksdb"); + tracing::info!( + "copying node state {} -> {}", + src_rocksdb_dir.display(), + export_directory.display() + ); + std::fs::create_dir_all(&export_directory)?; let copy_opts = fs_extra::dir::CopyOptions::new(); - home.push("rocksdb"); - let from = [home.as_path()]; - tracing::info!(?home, ?export_path, "copying from data dir to export dir",); - std::fs::create_dir_all(&export_path)?; - fs_extra::copy_items(&from, export_path.as_path(), ©_opts)?; - - tracing::info!("done copying"); - if !prune { - return Ok(()); + fs_extra::copy_items( + &[src_rocksdb_dir.as_path()], + export_directory.as_path(), + ©_opts, + )?; + tracing::info!("finished copying node state"); + + let dst_rocksdb_dir = export_directory.join("rocksdb"); + // If prune=true, then export-directory is required, because we must munge state prior + // to compressing. So we'll just mandate the presence of the --export-directory arg + // always. + if prune { + tracing::info!("pruning JMT tree"); + let export = Storage::load(dst_rocksdb_dir, SUBSTORE_PREFIXES.to_vec()).await?; + let _ = StateDelta::new(export.latest_snapshot()); + // TODO: + // - add utilities in `cnidarium` to prune a tree + // - apply the delta to the exported storage + // - apply checks: root hash, size, etc. + todo!() } - tracing::info!("pruning JMT tree"); - export_path.push("rocksdb"); - let export = Storage::load(export_path, SUBSTORE_PREFIXES.to_vec()).await?; - let _ = StateDelta::new(export.latest_snapshot()); - // TODO: - // - add utilities in `cnidarium` to prune a tree - // - apply the delta to the exported storage - // - apply checks: root hash, size, etc. - todo!() + // Compress to tarball if requested. + if let Some(archive_filepath) = export_archive { + pd::migrate::archive_directory( + dst_rocksdb_dir.clone(), + archive_filepath.clone(), + Some("rocksdb".to_owned()), + )?; + tracing::info!("export complete: {}", archive_filepath.display()); + } else { + // Provide friendly "OK" message that's still accurate without archiving. + tracing::info!("export complete: {}", export_directory.display()); + } } RootCommand::Migrate { - target_dir, + target_directory, genesis_start, + migrate_archive, } => { - tracing::info!("migrating state from {}", target_dir.display()); + tracing::info!("migrating state in {}", target_directory.display()); SimpleMigration - .migrate(target_dir.clone(), genesis_start) + .migrate(target_directory.clone(), genesis_start) .await .context("failed to upgrade state")?; + // Compress to tarball if requested. + if let Some(archive_filepath) = migrate_archive { + pd::migrate::archive_directory( + target_directory.clone(), + archive_filepath.clone(), + None, + )?; + tracing::info!("migration complete: {}", archive_filepath.display()); + } else { + // Provide friendly "OK" message that's still accurate without archiving. + tracing::info!("migration complete: {}", target_directory.display()); + } } } Ok(()) diff --git a/crates/bin/pd/src/migrate.rs b/crates/bin/pd/src/migrate.rs index 4a9cac548c..04feef53f4 100644 --- a/crates/bin/pd/src/migrate.rs +++ b/crates/bin/pd/src/migrate.rs @@ -4,6 +4,7 @@ //! node operators must coordinate to perform a chain upgrade. //! This module declares how local `pd` state should be altered, if at all, //! in order to be compatible with the network post-chain-upgrade. +use anyhow::Context; use std::path::PathBuf; use cnidarium::{StateDelta, StateWrite, Storage}; @@ -16,6 +17,10 @@ use penumbra_stake::{ use crate::testnet::generate::TestnetConfig; +use flate2::write::GzEncoder; +use flate2::Compression; +use std::fs::File; + /// The kind of migration that should be performed. pub enum Migration { /// No-op migration. @@ -36,9 +41,8 @@ impl Migration { match self { Migration::Noop => (), Migration::SimpleMigration => { - let mut db_path = path_to_export.clone(); - db_path.push("rocksdb"); - let storage = Storage::load(db_path, SUBSTORE_PREFIXES.to_vec()).await?; + let rocksdb_dir = path_to_export.join("rocksdb"); + let storage = Storage::load(rocksdb_dir, SUBSTORE_PREFIXES.to_vec()).await?; let export_state = storage.latest_snapshot(); let root_hash = export_state.root_hash().await.expect("can get root hash"); let app_hash_pre_migration: RootHash = root_hash.into(); @@ -97,12 +101,10 @@ impl Migration { let genesis_json = serde_json::to_string(&genesis).expect("can serialize genesis"); tracing::info!("genesis: {}", genesis_json); - let mut genesis_path = path_to_export.clone(); - genesis_path.push("genesis.json"); + let genesis_path = path_to_export.join("genesis.json"); std::fs::write(genesis_path, genesis_json).expect("can write genesis"); - let mut validator_state_path = path_to_export.clone(); - validator_state_path.push("priv_validator_state.json"); + let validator_state_path = path_to_export.join("priv_validator_state.json"); let fresh_validator_state = crate::testnet::generate::TestnetValidator::initial_state(); std::fs::write(validator_state_path, fresh_validator_state) @@ -113,3 +115,35 @@ impl Migration { Ok(()) } } + +/// Compress single directory to gzipped tar archive. Accepts an Option for naming +/// the subdir within the tar archive, which defaults to ".", meaning no nesting. +pub fn archive_directory( + src_directory: PathBuf, + archive_filepath: PathBuf, + subdir_within_archive: Option, +) -> anyhow::Result<()> { + // Don't clobber an existing target archive. + if archive_filepath.exists() { + tracing::error!( + "export archive filepath already exists: {}", + archive_filepath.display() + ); + anyhow::bail!("refusing to overwrite existing archive"); + } + + tracing::info!( + "creating archive {} -> {}", + src_directory.display(), + archive_filepath.display() + ); + let tarball_file = File::create(&archive_filepath) + .context("failed to create file for archive: check parent directory and permissions")?; + let enc = GzEncoder::new(tarball_file, Compression::default()); + let mut tarball = tar::Builder::new(enc); + let subdir_within_archive = subdir_within_archive.unwrap_or(String::from(".")); + tarball + .append_dir_all(subdir_within_archive, src_directory.as_path()) + .context("failed to package archive contents")?; + Ok(()) +} diff --git a/crates/bin/pd/src/testnet/join.rs b/crates/bin/pd/src/testnet/join.rs index d9d9fb772d..525390524c 100644 --- a/crates/bin/pd/src/testnet/join.rs +++ b/crates/bin/pd/src/testnet/join.rs @@ -8,6 +8,10 @@ use std::path::PathBuf; use tendermint_config::net::Address as TendermintAddress; use url::Url; +use flate2::read::GzDecoder; +use std::io::Write; +use tokio_stream::StreamExt; + use crate::testnet::config::{parse_tm_address, TestnetTendermintConfig}; use crate::testnet::generate::TestnetValidator; @@ -230,6 +234,81 @@ pub async fn fetch_peers(tm_url: &Url) -> anyhow::Result> Ok(seeds) } +/// Download a gzipped tarball from a URL, and extract its contents as the starting state +/// config for the fullnode. Allows bootstrapping from archived state, which is useful +/// for nodes joining after a chain upgrade has been performed. +/// +/// Supports archive files generated via `pd export`, which contain only the rocksdb dir, +/// and via `pd migrate`, which contain the rocksdb dir, new genesis content, and a private +/// validator state file. +/// +/// The `output_dir` should be the same argument as passed to `pd testnet --testnet-dir join`; +/// relative paths for pd and cometbft will be created from this base path. +pub async fn unpack_state_archive(archive_url: Url, output_dir: PathBuf) -> anyhow::Result<()> { + tracing::info!(%archive_url, "downloading compressed node state"); + // Download. + // Here we inspect HEAD so we can infer filename. + let response = reqwest::get(archive_url).await?; + let fname = response + .url() + .path_segments() + .and_then(|segments| segments.last()) + .and_then(|name| if name.is_empty() { None } else { Some(name) }) + .unwrap_or("pd-node-state-archive.tar.gz"); + + let archive_filepath = output_dir.join(fname); + let mut download_opts = std::fs::OpenOptions::new(); + download_opts.create_new(true).write(true); + let mut archive_file = download_opts.open(&archive_filepath)?; + + // Download via stream, in case file is too large to shove into RAM. + let mut stream = response.bytes_stream(); + while let Some(chunk_result) = stream.next().await { + let chunk = chunk_result?; + archive_file.write_all(&chunk)?; + } + archive_file.flush()?; + tracing::info!("download complete: {}", archive_filepath.display()); + + // Extract. + // Re-open downloaded file for unpacking, for a fresh filehandle. + let mut unpack_opts = std::fs::OpenOptions::new(); + unpack_opts.read(true); + let f = unpack_opts.open(&archive_filepath)?; + let tar = GzDecoder::new(f); + let mut archive = tar::Archive::new(tar); + // This dir-path building is duplicated in the config gen code. + let pd_home = output_dir.join("node0").join("pd"); + archive + .unpack(&pd_home) + .context("failed to extract tar.gz archive")?; + + // If the archive we consumed was generated via `pd migrate`, then it will contain + // a new genesis file and priv_validator_state.json, both of which should be applied + // over the generted cometbft config files. If the archive was generated via `pd export`, + // then those extra files will be missing, and only rocksdb data will be present. + let new_genesis = pd_home.join("genesis.json"); + let new_val_state = pd_home.join("priv_validator_state.json"); + let cometbft_dir = output_dir.join("node0").join("cometbft"); + let copy_opts = fs_extra::dir::CopyOptions::new().overwrite(true); + + if new_genesis.exists() { + tracing::info!(new_genesis = %new_genesis.display(), "copying new genesis content from archive"); + let f = vec![new_genesis]; + fs_extra::move_items(&f, cometbft_dir.join("config"), ©_opts)?; + } + if new_val_state.exists() { + tracing::info!(new_val_state = %new_val_state.display(), "copying new priv_validator_state.json content from archive"); + let f = vec![new_val_state]; + fs_extra::move_items(&f, cometbft_dir.join("data"), ©_opts)?; + } + + tracing::info!("archived node state unpacked to {}", pd_home.display()); + // Post-extraction, clean up the downloaded tarball. + std::fs::remove_file(archive_filepath)?; + Ok(()) +} + /// Check whether SocketAddress spec is likely to be externally-accessible. /// Filters out RFC1918 and loopback addresses. Requires an address and port. // TODO: This should return a Result, to be clearer about the expectation From b22813024c3be1d2aa416aa9ecc3f64f537dc8fd Mon Sep 17 00:00:00 2001 From: Conor Schaefer Date: Sun, 24 Mar 2024 20:33:36 -0700 Subject: [PATCH 006/168] fix: allow fetching archive-url from env var (#4093) Follow-up to [0]. We've tested several times the creation and use of archive urls via cli, but hadn't yet added logic to deploy new nodes from archive URLs, as well. This change worked for adding a new-joining node to a bespoke devnet post-upgrade. [0] https://github.com/penumbra-zone/penumbra/pull/4055 Co-authored-by: Conor Schaefer --- crates/bin/pd/src/cli.rs | 2 +- deployments/charts/penumbra-node/templates/statefulset.yaml | 4 ++++ deployments/charts/penumbra-node/values.yaml | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/bin/pd/src/cli.rs b/crates/bin/pd/src/cli.rs index 72977b9542..f8ee209d73 100644 --- a/crates/bin/pd/src/cli.rs +++ b/crates/bin/pd/src/cli.rs @@ -210,7 +210,7 @@ pub enum TestnetCommand { /// Optional URL of archived node state in .tar.gz format. The archive will be /// downloaded and extracted locally, allowing the node to join a network at a block height /// higher than 0. - #[clap(long)] + #[clap(long, env = "PENUMBRA_PD_ARCHIVE_URL")] archive_url: Option, /// Human-readable name to identify node on network diff --git a/deployments/charts/penumbra-node/templates/statefulset.yaml b/deployments/charts/penumbra-node/templates/statefulset.yaml index 5472743bc7..40f61e8c3a 100644 --- a/deployments/charts/penumbra-node/templates/statefulset.yaml +++ b/deployments/charts/penumbra-node/templates/statefulset.yaml @@ -125,6 +125,10 @@ spec: value: "{{ .Values.cometbft.config.p2p.max_num_outbound_peers }}" - name: PENUMBRA_COMETBFT_INDEXER value: "{{ .Values.cometbft.config.indexer }}" + {{- if .Values.penumbra_bootstrap_archive_url }} + - name: PENUMBRA_PD_ARCHIVE_URL + value: "{{ .Values.penumbra_bootstrap_archive_url }}" + {{- end }} {{- if .Values.postgres.credentialsSecretName }} - name: COMETBFT_POSTGRES_CONNECTION_URL valueFrom: diff --git a/deployments/charts/penumbra-node/values.yaml b/deployments/charts/penumbra-node/values.yaml index 7e25d6127e..04878f5496 100644 --- a/deployments/charts/penumbra-node/values.yaml +++ b/deployments/charts/penumbra-node/values.yaml @@ -14,6 +14,10 @@ image: # Consider using "preview" for bleeding edge, but make sure container tags match. penumbra_bootstrap_node_cometbft_rpc_url: "https://rpc.testnet.penumbra.zone" +# Whether to fetch a remote archive for loading historical state, +# e.g. post-upgrade. +penumbra_bootstrap_archive_url: "" + # Whether to preserve the LB service, in effect reserving the same IP # for subsequent deploys. This costs money! But sure is convenient. preserve_lb_svc: false From cdf449f82ebfe2300e48e5c0c2ed6e73f267df4c Mon Sep 17 00:00:00 2001 From: Henry de Valence Date: Mon, 25 Mar 2024 00:00:55 -0700 Subject: [PATCH 007/168] dex: redesign value circuit breaker (#4086) Closes #4025 This commit sketches a new mechanism but does not fix the existing tests; it needs to be picked up and pushed over the finish line. (We should be testing this). --------- Co-authored-by: Chris Czub --- .../component/dex/src/circuit_breaker/mod.rs | 5 - .../dex/src/circuit_breaker/value.rs | 250 ----------------- .../component/action_handler/position/open.rs | 9 +- .../action_handler/position/withdraw.rs | 8 +- .../dex/src/component/action_handler/swap.rs | 4 +- .../core/component/dex/src/component/arb.rs | 2 +- .../circuit_breaker/execution.rs | 0 .../dex/src/component/circuit_breaker/mod.rs | 5 + .../src/component/circuit_breaker/value.rs | 259 +++++++++++++++++ .../core/component/dex/src/component/dex.rs | 50 +++- .../core/component/dex/src/component/mod.rs | 3 + .../dex/src/component/position_manager.rs | 142 +--------- .../src/component/router/route_and_fill.rs | 34 +-- .../dex/src/component/router/tests.rs | 43 ++- .../core/component/dex/src/component/rpc.rs | 2 +- .../core/component/dex/src/component/tests.rs | 48 +++- crates/core/component/dex/src/event.rs | 26 ++ crates/core/component/dex/src/lib.rs | 2 - crates/core/component/dex/src/lp/position.rs | 18 +- crates/core/component/dex/src/state_key.rs | 6 + .../src/gen/penumbra.core.component.dex.v1.rs | 42 +++ .../penumbra.core.component.dex.v1.serde.rs | 264 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 374926 -> 376163 bytes .../penumbra/core/component/dex/v1/dex.proto | 20 ++ 24 files changed, 799 insertions(+), 443 deletions(-) delete mode 100644 crates/core/component/dex/src/circuit_breaker/mod.rs delete mode 100644 crates/core/component/dex/src/circuit_breaker/value.rs rename crates/core/component/dex/src/{ => component}/circuit_breaker/execution.rs (100%) create mode 100644 crates/core/component/dex/src/component/circuit_breaker/mod.rs create mode 100644 crates/core/component/dex/src/component/circuit_breaker/value.rs diff --git a/crates/core/component/dex/src/circuit_breaker/mod.rs b/crates/core/component/dex/src/circuit_breaker/mod.rs deleted file mode 100644 index 6dcbec4acc..0000000000 --- a/crates/core/component/dex/src/circuit_breaker/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod execution; -mod value; - -pub(crate) use execution::ExecutionCircuitBreaker; -pub(crate) use value::ValueCircuitBreaker; diff --git a/crates/core/component/dex/src/circuit_breaker/value.rs b/crates/core/component/dex/src/circuit_breaker/value.rs deleted file mode 100644 index 34a93ae165..0000000000 --- a/crates/core/component/dex/src/circuit_breaker/value.rs +++ /dev/null @@ -1,250 +0,0 @@ -use penumbra_asset::{asset::Id, Balance, Value}; -use penumbra_num::Amount; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct ValueCircuitBreaker { - balance: Balance, -} - -impl ValueCircuitBreaker { - pub fn tally(&mut self, balance: Balance) { - self.balance += balance; - } - - pub fn check(&self) -> anyhow::Result<()> { - // No assets should ever be "required" by the circuit breaker's - // internal balance tracking, only "provided". - if let Some(r) = self.balance.required().next() { - return Err(anyhow::anyhow!( - "balance for asset {} is negative: -{}", - r.asset_id, - r.amount - )); - } - - Ok(()) - } - - pub fn available(&self, asset_id: Id) -> Value { - self.balance - .provided() - .find(|b| b.asset_id == asset_id) - .unwrap_or(Value { - asset_id, - amount: Amount::from(0u64), - }) - } -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use crate::component::position_manager::Inner as _; - use crate::component::router::HandleBatchSwaps as _; - use crate::component::{StateReadExt as _, StateWriteExt as _}; - use crate::{ - component::{router::limit_buy, tests::TempStorageExt, PositionManager as _}, - state_key, DirectedUnitPair, - }; - use cnidarium::{ - ArcStateDeltaExt as _, StateDelta, StateRead as _, StateWrite as _, TempStorage, - }; - use penumbra_asset::{asset, Value}; - use penumbra_num::Amount; - use penumbra_proto::StateWriteProto as _; - use rand_core::OsRng; - - use crate::{ - lp::{position::Position, Reserves}, - DirectedTradingPair, - }; - - use super::*; - - // Ideally the update_position_aggregate_value in the PositionManager would be used - // but this is simpler for a quick unit test. - - #[test] - fn value_circuit_breaker() { - let mut value_circuit_breaker = ValueCircuitBreaker::default(); - - let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap(); - let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap(); - - let pair = DirectedTradingPair::new(gm.id(), gn.id()); - let reserves_1 = Reserves { - r1: 0u64.into(), - r2: 120_000u64.into(), - }; - - // A position with 120_000 gn and 0 gm. - let position_1 = Position::new( - OsRng, - pair, - 9u32, - 1_200_000u64.into(), - 1_000_000u64.into(), - reserves_1, - ); - - // Track the position in the circuit breaker. - let pair = position_1.phi.pair; - let new_a = position_1 - .reserves_for(pair.asset_1) - .expect("specified position should match provided trading pair"); - let new_b = position_1 - .reserves_for(pair.asset_2) - .expect("specified position should match provided trading pair"); - - let new_a = Balance::from(Value { - asset_id: pair.asset_1, - amount: new_a, - }); - let new_b = Balance::from(Value { - asset_id: pair.asset_2, - amount: new_b, - }); - value_circuit_breaker.tally(new_a); - value_circuit_breaker.tally(new_b.clone()); - - assert!(value_circuit_breaker.available(pair.asset_1).amount == 0u64.into()); - assert!(value_circuit_breaker.available(pair.asset_2).amount == 120_000u64.into()); - - // The circuit breaker should not trip. - assert!(value_circuit_breaker.check().is_ok()); - - // If the same amount of gn is taken out of the position, the circuit breaker should not trip. - value_circuit_breaker.tally(-new_b); - assert!(value_circuit_breaker.check().is_ok()); - - assert!(value_circuit_breaker.available(pair.asset_1).amount == 0u64.into()); - assert!(value_circuit_breaker.available(pair.asset_2).amount == 0u64.into()); - - // But if there's ever a negative amount of gn in the position, the circuit breaker should trip. - let one_b = Balance::from(Value { - asset_id: pair.asset_2, - amount: Amount::from(1u64), - }); - value_circuit_breaker.tally(-one_b); - assert!(value_circuit_breaker.check().is_err()); - assert!(value_circuit_breaker.available(pair.asset_1).amount == 0u64.into()); - assert!(value_circuit_breaker.available(pair.asset_2).amount == 0u64.into()); - } - - #[tokio::test] - async fn position_value_circuit_breaker() -> anyhow::Result<()> { - let _ = tracing_subscriber::fmt::try_init(); - let storage = TempStorage::new().await?.apply_minimal_genesis().await?; - let mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); - let mut state_tx = state.try_begin_transaction().unwrap(); - - let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap(); - let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap(); - - let pair_1 = DirectedUnitPair::new(gm.clone(), gn.clone()); - - let one = 1u64.into(); - let price1 = one; - // Create a position buying 1 gm with 1 gn (i.e. reserves will be 1gn). - let mut buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); - state_tx.put_position(buy_1.clone()).await.unwrap(); - - // Update the position to buy 1 gm with 2 gn (i.e. reserves will be 2gn). - buy_1.reserves.r2 = 2u64.into(); - state_tx.put_position(buy_1.clone()).await.unwrap(); - - // Pretend the position has been filled against and flipped, so there's no - // gn in the position and there is 2 gm. - buy_1.reserves.r1 = 2u64.into(); - buy_1.reserves.r2 = 0u64.into(); - - // This should not error, the circuit breaker should not trip. - state_tx.put_position(buy_1.clone()).await.unwrap(); - - // Pretend the position was overfilled. - let mut value_circuit_breaker: ValueCircuitBreaker = match state_tx - .nonverifiable_get_raw(state_key::aggregate_value().as_bytes()) - .await - .expect("able to retrieve value circuit breaker from nonverifiable storage") - { - Some(bytes) => serde_json::from_slice(&bytes).expect( - "able to deserialize stored value circuit breaker from nonverifiable storage", - ), - None => panic!("should have a circuit breaker present"), - }; - - // Wipe out the value in the circuit breaker, so that any outflows should trip it. - value_circuit_breaker.balance = Balance::default(); - state_tx.nonverifiable_put_raw( - state_key::aggregate_value().as_bytes().to_vec(), - serde_json::to_vec(&value_circuit_breaker) - .expect("able to serialize value circuit breaker for nonverifiable storage"), - ); - - // This should error, since there is no balance available to close out the position. - buy_1.state = crate::lp::position::State::Closed; - assert!(state_tx.put_position(buy_1).await.is_err()); - - Ok(()) - } - - #[tokio::test] - #[should_panic(expected = "balance for asset")] - async fn batch_swap_circuit_breaker() { - let _ = tracing_subscriber::fmt::try_init(); - let storage = TempStorage::new() - .await - .expect("able to create storage") - .apply_minimal_genesis() - .await - .expect("able to apply genesis"); - let mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); - let mut state_tx = state.try_begin_transaction().unwrap(); - - let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap(); - let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap(); - - let pair_1 = DirectedUnitPair::new(gm.clone(), gn.clone()); - - // Manually put a position without calling `put_position` so that the - // circuit breaker is not aware of the position's value. Then, handling a batch - // swap that fills against this position should result in an error. - let one = 1u64.into(); - let price1 = one; - // Create a position buying 1 gm with 1 gn (i.e. reserves will be 1gn). - let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); - - let id = buy_1.id(); - - let position = state_tx.handle_limit_order(&None, buy_1); - state_tx.index_position_by_price(&position); - state_tx - .update_available_liquidity(&position, &None) - .await - .expect("able to update liquidity"); - state_tx.put(state_key::position_by_id(&id), position); - - // Now there's a position in the state, but the circuit breaker is not aware of it. - let trading_pair = pair_1.into_directed_trading_pair().into(); - let mut swap_flow = state_tx.swap_flow(&trading_pair); - - assert!(trading_pair.asset_1() == gm.id()); - - // Add the amount of each asset being swapped to the batch swap flow. - swap_flow.0 += gm.value(5u32.into()).amount; - swap_flow.1 += 0u32.into(); - - // Set the batch swap flow for the trading pair. - state_tx.put_swap_flow(&trading_pair, swap_flow.clone()); - state_tx.apply(); - - // This call should panic due to the outflow of gn not being covered by the circuit breaker. - let routing_params = state.routing_params().await.unwrap(); - state - .handle_batch_swaps(trading_pair, swap_flow, 0, 0, routing_params) - .await - .expect("unable to process batch swaps"); - } -} diff --git a/crates/core/component/dex/src/component/action_handler/position/open.rs b/crates/core/component/dex/src/component/action_handler/position/open.rs index 2474d50290..588c8972b7 100644 --- a/crates/core/component/dex/src/component/action_handler/position/open.rs +++ b/crates/core/component/dex/src/component/action_handler/position/open.rs @@ -5,7 +5,7 @@ use cnidarium_component::ActionHandler; use penumbra_proto::StateWriteProto as _; use crate::{ - component::{PositionManager, PositionRead}, + component::{PositionManager, PositionRead, ValueCircuitBreaker}, event, lp::{action::PositionOpen, position}, }; @@ -33,6 +33,13 @@ impl ActionHandler for PositionOpen { async fn check_and_execute(&self, mut state: S) -> Result<()> { // Validate that the position ID doesn't collide state.check_position_id_unused(&self.position.id()).await?; + + // Credit the DEX for the inflows from this position. + // TODO: in a future PR, split current PositionManager to PositionManagerInner + // and fold this into a position open method + state.vcb_credit(self.position.reserves_1()).await?; + state.vcb_credit(self.position.reserves_2()).await?; + state.put_position(self.position.clone()).await?; state.record_proto(event::position_open(self)); Ok(()) diff --git a/crates/core/component/dex/src/component/action_handler/position/withdraw.rs b/crates/core/component/dex/src/component/action_handler/position/withdraw.rs index aed55aa0cd..a012ae4c9c 100644 --- a/crates/core/component/dex/src/component/action_handler/position/withdraw.rs +++ b/crates/core/component/dex/src/component/action_handler/position/withdraw.rs @@ -7,7 +7,7 @@ use decaf377::Fr; use penumbra_proto::StateWriteProto; use crate::{ - component::{PositionManager, PositionRead}, + component::{PositionManager, PositionRead, ValueCircuitBreaker}, event, lp::{action::PositionWithdraw, position, Reserves}, }; @@ -90,6 +90,12 @@ impl ActionHandler for PositionWithdraw { // the current reserves. state.record_proto(event::position_withdraw(self, &metadata)); + // Debit the DEX for the outflows from this position. + // TODO: in a future PR, split current PositionManager to PositionManagerInner + // and fold this into a position open method + state.vcb_debit(metadata.reserves_1()).await?; + state.vcb_debit(metadata.reserves_2()).await?; + // Finally, update the position. This has two steps: // - update the state with the correct sequence number; // - zero out the reserves, to prevent double-withdrawals. diff --git a/crates/core/component/dex/src/component/action_handler/swap.rs b/crates/core/component/dex/src/component/action_handler/swap.rs index b1dce652dc..b735a3b295 100644 --- a/crates/core/component/dex/src/component/action_handler/swap.rs +++ b/crates/core/component/dex/src/component/action_handler/swap.rs @@ -47,7 +47,9 @@ impl ActionHandler for Swap { swap_flow.1 += swap.body.delta_2_i; // Set the batch swap flow for the trading pair. - state.put_swap_flow(&swap.body.trading_pair, swap_flow); + state + .put_swap_flow(&swap.body.trading_pair, swap_flow) + .await?; // Record the swap commitment in the state. let source = state.get_current_source().expect("source is set"); diff --git a/crates/core/component/dex/src/component/arb.rs b/crates/core/component/dex/src/component/arb.rs index b1d9009de5..b802ca2279 100644 --- a/crates/core/component/dex/src/component/arb.rs +++ b/crates/core/component/dex/src/component/arb.rs @@ -8,7 +8,7 @@ use penumbra_proto::StateWriteProto as _; use penumbra_sct::component::clock::EpochRead; use tracing::instrument; -use crate::{event, ExecutionCircuitBreaker, SwapExecution}; +use crate::{component::ExecutionCircuitBreaker, event, SwapExecution}; use super::{ router::{RouteAndFill, RoutingParams}, diff --git a/crates/core/component/dex/src/circuit_breaker/execution.rs b/crates/core/component/dex/src/component/circuit_breaker/execution.rs similarity index 100% rename from crates/core/component/dex/src/circuit_breaker/execution.rs rename to crates/core/component/dex/src/component/circuit_breaker/execution.rs diff --git a/crates/core/component/dex/src/component/circuit_breaker/mod.rs b/crates/core/component/dex/src/component/circuit_breaker/mod.rs new file mode 100644 index 0000000000..448223c17c --- /dev/null +++ b/crates/core/component/dex/src/component/circuit_breaker/mod.rs @@ -0,0 +1,5 @@ +mod execution; +mod value; + +pub use execution::ExecutionCircuitBreaker; +pub use value::ValueCircuitBreaker; diff --git a/crates/core/component/dex/src/component/circuit_breaker/value.rs b/crates/core/component/dex/src/component/circuit_breaker/value.rs new file mode 100644 index 0000000000..eb650207a7 --- /dev/null +++ b/crates/core/component/dex/src/component/circuit_breaker/value.rs @@ -0,0 +1,259 @@ +use anyhow::{anyhow, Result}; +use cnidarium::StateWrite; +use penumbra_asset::Value; +use penumbra_num::Amount; +use penumbra_proto::{StateReadProto, StateWriteProto}; +use tonic::async_trait; + +use crate::{event, state_key}; + +/// Tracks the aggregate value of deposits in the DEX. +#[async_trait] +pub trait ValueCircuitBreaker: StateWrite { + /// Credits a deposit into the DEX. + async fn vcb_credit(&mut self, value: Value) -> Result<()> { + let balance: Amount = self + .get(&state_key::value_balance(&value.asset_id)) + .await? + .unwrap_or_default(); + let new_balance = balance + .checked_add(&value.amount) + .ok_or_else(|| anyhow!("overflowed balance while crediting value circuit breaker"))?; + self.put(state_key::value_balance(&value.asset_id), new_balance); + + self.record_proto(event::vcb_credit(value.asset_id, balance, new_balance)); + Ok(()) + } + + /// Debits a deposit from the DEX. + async fn vcb_debit(&mut self, value: Value) -> Result<()> { + let balance: Amount = self + .get(&state_key::value_balance(&value.asset_id)) + .await? + .unwrap_or_default(); + let new_balance = balance + .checked_sub(&value.amount) + .ok_or_else(|| anyhow!("underflowed balance while debiting value circuit breaker"))?; + self.put(state_key::value_balance(&value.asset_id), new_balance); + + self.record_proto(event::vcb_debit(value.asset_id, balance, new_balance)); + Ok(()) + } +} + +impl ValueCircuitBreaker for T {} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use crate::component::position_manager::Inner as _; + use crate::component::router::HandleBatchSwaps as _; + use crate::component::{StateReadExt as _, StateWriteExt as _}; + use crate::lp::plan::PositionWithdrawPlan; + use crate::{ + component::{router::limit_buy, tests::TempStorageExt, PositionManager as _}, + state_key, DirectedUnitPair, + }; + use crate::{BatchSwapOutputData, PositionOpen}; + use cnidarium::{ArcStateDeltaExt as _, StateDelta, TempStorage}; + use cnidarium_component::ActionHandler as _; + use penumbra_asset::asset; + use penumbra_num::Amount; + use penumbra_proto::StateWriteProto as _; + use penumbra_sct::component::clock::EpochManager as _; + use penumbra_sct::component::source::SourceContext as _; + use penumbra_sct::epoch::Epoch; + + use super::*; + + #[tokio::test] + async fn value_circuit_breaker() -> anyhow::Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + let storage = TempStorage::new().await?.apply_minimal_genesis().await?; + let mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); + let mut state_tx = state.try_begin_transaction().unwrap(); + + let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap(); + let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap(); + let test_usd = asset::Cache::with_known_assets() + .get_unit("test_usd") + .unwrap(); + + // A credit followed by a debit of the same amount should succeed. + // Credit 100 gm. + state_tx.vcb_credit(gm.value(100u64.into())).await?; + // Credit 100 gn. + state_tx.vcb_credit(gn.value(100u64.into())).await?; + + // Debit 100 gm. + state_tx.vcb_debit(gm.value(100u64.into())).await?; + // Debit 100 gn. + state_tx.vcb_debit(gn.value(100u64.into())).await?; + + // Debiting an additional gm should fail. + assert!(state_tx.vcb_debit(gm.value(1u64.into())).await.is_err()); + + // Debiting an asset that hasn't been credited should also fail. + assert!(state_tx + .vcb_debit(test_usd.value(1u64.into())) + .await + .is_err()); + + Ok(()) + } + + #[tokio::test] + async fn position_value_circuit_breaker() -> anyhow::Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + let storage = TempStorage::new().await?.apply_minimal_genesis().await?; + let mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); + let mut state_tx = state.try_begin_transaction().unwrap(); + + let height = 1; + + // 1. Simulate BeginBlock + + state_tx.put_epoch_by_height( + height, + Epoch { + index: 0, + start_height: 0, + }, + ); + state_tx.put_block_height(height); + state_tx.apply(); + + let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap(); + let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap(); + + let pair_1 = DirectedUnitPair::new(gm.clone(), gn.clone()); + + let one = 1u64.into(); + let price1 = one; + // Create a position buying 1 gm with 1 gn (i.e. reserves will be 1gn). + let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); + + // Create the PositionOpen action + let pos_open = PositionOpen { + position: buy_1.clone(), + }; + + // Execute the PositionOpen action. + pos_open.check_stateless(()).await?; + pos_open.check_historical(state.clone()).await?; + let mut state_tx = state.try_begin_transaction().unwrap(); + state_tx.put_mock_source(1u8); + pos_open.check_and_execute(&mut state_tx).await?; + state_tx.apply(); + + // Set the output data for the block to 1 gn and 0 gm. + // This should not error, the circuit breaker should not trip. + let mut state_tx = state.try_begin_transaction().unwrap(); + state_tx + .set_output_data( + BatchSwapOutputData { + delta_1: 0u64.into(), + delta_2: 1u64.into(), + lambda_1: 0u64.into(), + lambda_2: 0u64.into(), + unfilled_1: 0u64.into(), + unfilled_2: 0u64.into(), + height: 1, + trading_pair: pair_1.into_directed_trading_pair().into(), + epoch_starting_height: 0, + }, + None, + None, + ) + .await?; + + // Pretend the position was overfilled. + + // Wipe out the gm value in the circuit breaker, so that any outflows should trip it. + state_tx.put(state_key::value_balance(&gm.id()), Amount::from(0u64)); + + // Create the PositionWithdraw action + let pos_withdraw_plan = PositionWithdrawPlan { + position_id: buy_1.id(), + reserves: buy_1.reserves, + sequence: 1, + pair: pair_1.into_directed_trading_pair().into(), + rewards: vec![], + }; + + let pos_withdraw = pos_withdraw_plan.position_withdraw(); + + // Execute the PositionWithdraw action. + pos_withdraw.check_stateless(()).await?; + pos_withdraw.check_historical(state.clone()).await?; + let mut state_tx = state.try_begin_transaction().unwrap(); + state_tx.put_mock_source(1u8); + // This should error, since there is no balance available to withdraw the position. + assert!(pos_withdraw.check_and_execute(&mut state_tx).await.is_err()); + state_tx.apply(); + + Ok(()) + } + + #[tokio::test] + #[should_panic(expected = "underflowed balance while debiting value circuit breaker")] + async fn batch_swap_circuit_breaker() { + let _ = tracing_subscriber::fmt::try_init(); + let storage = TempStorage::new() + .await + .expect("able to create storage") + .apply_minimal_genesis() + .await + .expect("able to apply genesis"); + let mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); + let mut state_tx = state.try_begin_transaction().unwrap(); + + let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap(); + let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap(); + + let pair_1 = DirectedUnitPair::new(gm.clone(), gn.clone()); + + // Manually put a position without calling `put_position` so that the + // circuit breaker is not aware of the position's value. Then, handling a batch + // swap that fills against this position should result in an error. + let one = 1u64.into(); + let price1 = one; + // Create a position buying 1 gm with 1 gn (i.e. reserves will be 1gn). + let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); + + let id = buy_1.id(); + + let position = state_tx.handle_limit_order(&None, buy_1); + state_tx.index_position_by_price(&position); + state_tx + .update_available_liquidity(&position, &None) + .await + .expect("able to update liquidity"); + state_tx.put(state_key::position_by_id(&id), position); + + // Now there's a position in the state, but the circuit breaker is not aware of it. + let trading_pair = pair_1.into_directed_trading_pair().into(); + let mut swap_flow = state_tx.swap_flow(&trading_pair); + + assert!(trading_pair.asset_1() == gm.id()); + + // Add the amount of each asset being swapped to the batch swap flow. + swap_flow.0 += gm.value(5u32.into()).amount; + swap_flow.1 += 0u32.into(); + + // Set the batch swap flow for the trading pair. + state_tx + .put_swap_flow(&trading_pair, swap_flow.clone()) + .await + .unwrap(); + state_tx.apply(); + + let routing_params = state.routing_params().await.unwrap(); + // This call should panic due to the outflow of gn not being covered by the circuit breaker. + state + .handle_batch_swaps(trading_pair, swap_flow, 0, 0, routing_params) + .await + .expect("unable to process batch swaps"); + } +} diff --git a/crates/core/component/dex/src/component/dex.rs b/crates/core/component/dex/src/component/dex.rs index f070d98bd1..38456735d6 100644 --- a/crates/core/component/dex/src/component/dex.rs +++ b/crates/core/component/dex/src/component/dex.rs @@ -18,7 +18,7 @@ use crate::{ use super::{ router::{HandleBatchSwaps, RoutingParams}, - Arbitrage, PositionManager, + Arbitrage, PositionManager, ValueCircuitBreaker, }; pub struct Dex {} @@ -209,12 +209,29 @@ pub trait StateWriteExt: StateWrite + StateReadExt { self.object_put(state_key::config::dex_params_updated(), ()) } - fn set_output_data( + async fn set_output_data( &mut self, output_data: BatchSwapOutputData, swap_execution_1_for_2: Option, swap_execution_2_for_1: Option, - ) { + ) -> Result<()> { + // Debit the DEX for the swap outflows. + // Note that since we credited the DEX for _all_ inflows, we need to debit the + // unfilled amounts as well as the filled amounts. + // + // In the case of a value inflation bug, the debit call will return an underflow + // error, which will halt the chain. + self.vcb_debit(Value { + amount: output_data.unfilled_1 + output_data.lambda_1, + asset_id: output_data.trading_pair.asset_1, + }) + .await?; + self.vcb_debit(Value { + amount: output_data.unfilled_2 + output_data.lambda_2, + asset_id: output_data.trading_pair.asset_2, + }) + .await?; + // Write the output data to the state under a known key, for querying, ... let height = output_data.height; let trading_pair = output_data.trading_pair; @@ -247,17 +264,40 @@ pub trait StateWriteExt: StateWrite + StateReadExt { swap_execution_1_for_2, swap_execution_2_for_1, )); + + Ok(()) } fn set_arb_execution(&mut self, height: u64, execution: SwapExecution) { self.put(state_key::arb_execution(height), execution); } - fn put_swap_flow(&mut self, trading_pair: &TradingPair, swap_flow: SwapFlow) { + async fn put_swap_flow( + &mut self, + trading_pair: &TradingPair, + swap_flow: SwapFlow, + ) -> Result<()> { + // Credit the DEX for the swap inflows. + // + // Note that we credit the DEX for _all_ inflows, since we don't know + // how much will eventually be filled. + self.vcb_credit(Value { + amount: swap_flow.0, + asset_id: trading_pair.asset_1, + }) + .await?; + self.vcb_credit(Value { + amount: swap_flow.1, + asset_id: trading_pair.asset_2, + }) + .await?; + // TODO: replace with IM struct later let mut swap_flows = self.swap_flows(); swap_flows.insert(*trading_pair, swap_flow); - self.object_put(state_key::swap_flows(), swap_flows) + self.object_put(state_key::swap_flows(), swap_flows); + + Ok(()) } } diff --git a/crates/core/component/dex/src/component/mod.rs b/crates/core/component/dex/src/component/mod.rs index 11b8eb217b..871bd67748 100644 --- a/crates/core/component/dex/src/component/mod.rs +++ b/crates/core/component/dex/src/component/mod.rs @@ -8,6 +8,7 @@ pub mod router; mod action_handler; mod arb; +pub(crate) mod circuit_breaker; mod dex; mod flow; pub(crate) mod position_manager; @@ -15,6 +16,8 @@ mod swap_manager; pub use self::metrics::register_metrics; pub use arb::Arbitrage; +pub use circuit_breaker::ExecutionCircuitBreaker; +pub(crate) use circuit_breaker::ValueCircuitBreaker; pub use dex::{Dex, StateReadExt, StateWriteExt}; pub use position_manager::{PositionManager, PositionRead}; pub use swap_manager::SwapManager; diff --git a/crates/core/component/dex/src/component/position_manager.rs b/crates/core/component/dex/src/component/position_manager.rs index a004547db1..9854418fd6 100644 --- a/crates/core/component/dex/src/component/position_manager.rs +++ b/crates/core/component/dex/src/component/position_manager.rs @@ -7,12 +7,11 @@ use async_trait::async_trait; use cnidarium::{EscapedByteSlice, StateRead, StateWrite}; use futures::Stream; use futures::StreamExt; -use penumbra_asset::{asset, Balance, Value}; +use penumbra_asset::asset; use penumbra_num::Amount; use penumbra_proto::DomainType; use penumbra_proto::{StateReadProto, StateWriteProto}; -use crate::circuit_breaker::ValueCircuitBreaker; use crate::lp::position::State; use crate::{ lp::position::{self, Position}, @@ -158,10 +157,6 @@ pub trait PositionManager: StateWrite + PositionRead { // Update the available liquidity for this position's trading pair. self.update_available_liquidity(&position, &prev).await?; - // Update the value circuit breaker's aggregate account. - self.update_position_aggregate_value(&position, &prev) - .await?; - self.put(state_key::position_by_id(&id), position); Ok(()) } @@ -463,140 +458,5 @@ pub(crate) trait Inner: StateWrite { Ok(()) } - - /// Tracks the total token supply deposited in positions for all assets to ensure - /// asset value conservation (i.e. that more assets can't come out of positions than - /// were deposited). - async fn update_position_aggregate_value( - &mut self, - position: &Position, - prev_position: &Option, - ) -> Result<()> { - tracing::debug!( - ?position, - ?prev_position, - "updating position aggregate value" - ); - - // Find the difference in the amounts of assets A and B, based on the state of the position being stored, - // and the previous state of the position. - let (net_change_for_a, net_change_for_b) = match (position.state, prev_position) { - (State::Opened, None) => { - // The position is newly opened, so the change is the full amount of assets A and B. - - // Use the new reserves to compute `new_position_contribution`, - // the amount of asset A contributed by the position (i.e. the reserves of asset A). - let pair = position.phi.pair; - let new_a = position - .reserves_for(pair.asset_1) - .expect("specified position should match provided trading pair"); - let new_b = position - .reserves_for(pair.asset_2) - .expect("specified position should match provided trading pair"); - - let new_a = Balance::from(Value { - asset_id: pair.asset_1, - amount: new_a, - }); - let new_b = Balance::from(Value { - asset_id: pair.asset_2, - amount: new_b, - }); - (new_a, new_b) - } - (State::Opened, Some(prev)) => { - // The position is still open however the reserves have changed, so the change is the difference - // between the previous reserves and the new reserves. - let pair = position.phi.pair; - let new_a = Balance::from(Value { - asset_id: pair.asset_1, - amount: position - .reserves_for(pair.asset_1) - .expect("specified position should match provided trading pair"), - }); - let new_b = Balance::from(Value { - asset_id: pair.asset_2, - amount: position - .reserves_for(pair.asset_2) - .expect("specified position should match provided trading pair"), - }); - let old_a = Balance::from(Value { - asset_id: pair.asset_1, - amount: prev - .reserves_for(pair.asset_1) - .expect("specified position should match provided trading pair"), - }); - let old_b = Balance::from(Value { - asset_id: pair.asset_2, - amount: prev - .reserves_for(pair.asset_2) - .expect("specified position should match provided trading pair"), - }); - - (new_a - old_a, new_b - old_b) - } - (State::Closed, Some(prev)) => { - // The previous amount of assets A and B should be subtracted from the aggregate value. - - let pair = position.phi.pair; - let old_a = prev - .reserves_for(pair.asset_1) - .expect("specified position should match provided trading pair"); - let old_b = prev - .reserves_for(pair.asset_2) - .expect("specified position should match provided trading pair"); - - let old_a = Balance::from(Value { - asset_id: pair.asset_1, - amount: old_a, - }); - let old_b = Balance::from(Value { - asset_id: pair.asset_2, - amount: old_b, - }); - // The position is closed, so the change is the negative of the previous reserves. - (-old_a, -old_b) - } - (State::Withdrawn { .. }, _) | (State::Closed, None) => { - // The position already went through the `Closed` state or was opened in the `Closed` state, so its contribution has already been subtracted. - return Ok(()); - } - }; - - tracing::debug!( - ?position, - ?net_change_for_a, - ?net_change_for_b, - "updating position assets' aggregate balances" - ); - - let mut value_circuit_breaker: ValueCircuitBreaker = match self - .nonverifiable_get_raw(state_key::aggregate_value().as_bytes()) - .await - .expect("able to retrieve value circuit breaker from nonverifiable storage") - { - Some(bytes) => serde_json::from_slice(&bytes).expect( - "able to deserialize stored value circuit breaker from nonverifiable storage", - ), - None => ValueCircuitBreaker::default(), - }; - - // Add the change to the value circuit breaker for assets A and B. - value_circuit_breaker.tally(net_change_for_a); - value_circuit_breaker.tally(net_change_for_b); - - // Confirm that the value circuit breaker is still within the limits. - // This call will panic if the value circuit breaker detects inflation. - value_circuit_breaker.check()?; - - // Store the value circuit breaker back to nonconsensus storage with the updated tallies. - self.nonverifiable_put_raw( - state_key::aggregate_value().as_bytes().to_vec(), - serde_json::to_vec(&value_circuit_breaker) - .expect("able to serialize value circuit breaker for nonverifiable storage"), - ); - - Ok(()) - } } impl Inner for T {} diff --git a/crates/core/component/dex/src/component/router/route_and_fill.rs b/crates/core/component/dex/src/component/router/route_and_fill.rs index 19671b4edd..b18a786200 100644 --- a/crates/core/component/dex/src/component/router/route_and_fill.rs +++ b/crates/core/component/dex/src/component/router/route_and_fill.rs @@ -8,14 +8,13 @@ use penumbra_num::Amount; use tracing::instrument; use crate::{ - circuit_breaker::ValueCircuitBreaker, component::{ flow::SwapFlow, router::{FillRoute, PathSearch, RoutingParams}, - PositionManager, StateWriteExt, + ExecutionCircuitBreaker, PositionManager, StateWriteExt, }, lp::position::MAX_RESERVE_AMOUNT, - state_key, BatchSwapOutputData, ExecutionCircuitBreaker, SwapExecution, TradingPair, + BatchSwapOutputData, SwapExecution, TradingPair, }; use super::fill_route::FillError; @@ -49,19 +48,6 @@ pub trait HandleBatchSwaps: StateWrite + Sized { tracing::debug!(?delta_1, ?delta_2, ?trading_pair, "decrypted batch swaps"); let execution_circuit_breaker = ExecutionCircuitBreaker::default(); - // Fetch the ValueCircuitBreaker prior to calling `route_and_fill`, so - // we know the total aggregate amount of each asset prior to executing and - // can ensure the total outflows don't exceed the total balances. - let value_circuit_breaker: ValueCircuitBreaker = match self - .nonverifiable_get_raw(state_key::aggregate_value().as_bytes()) - .await - .expect("able to retrieve value circuit breaker from nonverifiable storage") - { - Some(bytes) => serde_json::from_slice(&bytes).expect( - "able to deserialize stored value circuit breaker from nonverifiable storage", - ), - None => ValueCircuitBreaker::default(), - }; let swap_execution_1_for_2 = if delta_1.value() > 0 { Some( @@ -121,19 +107,6 @@ pub trait HandleBatchSwaps: StateWrite + Sized { unfilled_2, }; - // Check that the output data doesn't exceed the ValueCircuitBreaker's quantities - // (i.e. we didn't outflow more value than existed within liquidity positions). - let available_asset_1 = value_circuit_breaker.available(trading_pair.asset_1()); - let available_asset_2 = value_circuit_breaker.available(trading_pair.asset_2()); - assert!( - output_data.lambda_1 <= available_asset_1.amount, - "asset 1 outflow exceeds available balance" - ); - assert!( - output_data.lambda_2 <= available_asset_2.amount, - "asset 2 outflow exceeds available balance" - ); - // Fetch the swap execution object that should have been modified during the routing and filling. tracing::debug!( ?output_data, @@ -142,7 +115,8 @@ pub trait HandleBatchSwaps: StateWrite + Sized { ); Arc::get_mut(self) .expect("expected state to have no other refs") - .set_output_data(output_data, swap_execution_1_for_2, swap_execution_2_for_1); + .set_output_data(output_data, swap_execution_1_for_2, swap_execution_2_for_1) + .await?; Ok(()) } diff --git a/crates/core/component/dex/src/component/router/tests.rs b/crates/core/component/dex/src/component/router/tests.rs index 226974ba71..81ca29dc37 100644 --- a/crates/core/component/dex/src/component/router/tests.rs +++ b/crates/core/component/dex/src/component/router/tests.rs @@ -8,6 +8,7 @@ use penumbra_num::{fixpoint::U128x128, Amount}; use rand_core::OsRng; use std::sync::Arc; +use crate::component::ValueCircuitBreaker; use crate::lp::SellOrder; use crate::DexParameters; use crate::{ @@ -988,6 +989,19 @@ async fn best_position_route_and_fill() -> anyhow::Result<()> { // Create a single 1:1 gn:penumbra position (i.e. buy 1 gn at 1 penumbra). let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), 1u64.into()); state_tx.put_position(buy_1).await.unwrap(); + // TODO: later, this should be folded into an open_position method + state_tx + .vcb_credit(Value { + asset_id: gn.id(), + amount: Amount::from(1u64) * gn.unit_amount(), + }) + .await?; + state_tx + .vcb_credit(Value { + asset_id: penumbra.id(), + amount: Amount::from(1u64) * penumbra.unit_amount(), + }) + .await?; state_tx.apply(); // We should be able to call path_search and route through that position. @@ -1015,7 +1029,9 @@ async fn best_position_route_and_fill() -> anyhow::Result<()> { // Set the batch swap flow for the trading pair. Arc::get_mut(&mut state) .unwrap() - .put_swap_flow(&trading_pair, swap_flow.clone()); + .put_swap_flow(&trading_pair, swap_flow.clone()) + .await + .unwrap(); let routing_params = state.routing_params().await.unwrap(); state .handle_batch_swaps(trading_pair, swap_flow, 0u32.into(), 0, routing_params) @@ -1061,6 +1077,27 @@ async fn multi_hop_route_and_fill() -> anyhow::Result<()> { let pair_gn_gm = DirectedUnitPair::new(gn.clone(), gm.clone()); let pair_gm_penumbra = DirectedUnitPair::new(gm.clone(), penumbra.clone()); + // TEMP TODO: disable VCB for this test. Later, remove this code once we restructure + // the position manager. + let infinite_gm = Value { + asset_id: gm.id(), + amount: Amount::from(100000u128) * gm.unit_amount(), + }; + + let infinite_gn = Value { + asset_id: gn.id(), + amount: Amount::from(100000u128) * gn.unit_amount(), + }; + + let infinite_penumbra = Value { + asset_id: penumbra.id(), + amount: Amount::from(100000u128) * penumbra.unit_amount(), + }; + + state_tx.vcb_credit(infinite_gm).await?; + state_tx.vcb_credit(infinite_gn).await?; + state_tx.vcb_credit(infinite_penumbra).await?; + // Create a 2:1 penumbra:gm position (i.e. buy 20 gm at 2 penumbra each). let buy_1 = limit_buy_pq( pair_gm_penumbra.clone(), @@ -1154,7 +1191,9 @@ async fn multi_hop_route_and_fill() -> anyhow::Result<()> { // Set the batch swap flow for the trading pair. Arc::get_mut(&mut state) .unwrap() - .put_swap_flow(&trading_pair, swap_flow.clone()); + .put_swap_flow(&trading_pair, swap_flow.clone()) + .await + .unwrap(); let routing_params = state.routing_params().await.unwrap(); state .handle_batch_swaps(trading_pair, swap_flow, 0u32.into(), 0, routing_params) diff --git a/crates/core/component/dex/src/component/rpc.rs b/crates/core/component/dex/src/component/rpc.rs index 64aaccc2f4..ed3e6914d0 100644 --- a/crates/core/component/dex/src/component/rpc.rs +++ b/crates/core/component/dex/src/component/rpc.rs @@ -23,7 +23,7 @@ use penumbra_proto::{ DomainType, StateReadProto, }; -use crate::ExecutionCircuitBreaker; +use super::ExecutionCircuitBreaker; use crate::{ lp::position::{self, Position}, state_key, DirectedTradingPair, SwapExecution, TradingPair, diff --git a/crates/core/component/dex/src/component/tests.rs b/crates/core/component/dex/src/component/tests.rs index 3f7b727a7a..1aca0cf75b 100644 --- a/crates/core/component/dex/src/component/tests.rs +++ b/crates/core/component/dex/src/component/tests.rs @@ -10,6 +10,7 @@ use rand_core::OsRng; //use crate::TempStorageExt; +use crate::component::ValueCircuitBreaker as _; use crate::lp::action::PositionOpen; use crate::DexParameters; use crate::{ @@ -560,6 +561,21 @@ async fn swap_execution_tests() -> anyhow::Result<()> { let pair_gn_penumbra = DirectedUnitPair::new(gn.clone(), penumbra.clone()); + // TEMP TODO: disable VCB for this test. Later, remove this code once we restructure + // the position manager. + let infinite_gn = Value { + asset_id: gn.id(), + amount: Amount::from(100000u128) * gn.unit_amount(), + }; + + let infinite_penumbra = Value { + asset_id: penumbra.id(), + amount: Amount::from(100000u128) * penumbra.unit_amount(), + }; + + state_tx.vcb_credit(infinite_gn).await?; + state_tx.vcb_credit(infinite_penumbra).await?; + // Create a single 1:1 gn:penumbra position (i.e. buy 1 gn at 1 penumbra). let buy_1 = limit_buy(pair_gn_penumbra.clone(), 1u64.into(), 1u64.into()); state_tx.put_position(buy_1).await.unwrap(); @@ -579,7 +595,9 @@ async fn swap_execution_tests() -> anyhow::Result<()> { // Set the batch swap flow for the trading pair. Arc::get_mut(&mut state) .unwrap() - .put_swap_flow(&trading_pair, swap_flow.clone()); + .put_swap_flow(&trading_pair, swap_flow.clone()) + .await + .unwrap(); let routing_params = state.routing_params().await.unwrap(); state .handle_batch_swaps(trading_pair, swap_flow, 0, 0, routing_params) @@ -625,6 +643,30 @@ async fn swap_execution_tests() -> anyhow::Result<()> { .get_unit("test_usd") .unwrap(); + // TEMP TODO: disable VCB for this test. Later, remove this code once we restructure + // the position manager. + let infinite_gn = Value { + asset_id: gn.id(), + amount: Amount::from(100000u128) * gn.unit_amount(), + }; + let infinite_gm = Value { + asset_id: gm.id(), + amount: Amount::from(100000u128) * gm.unit_amount(), + }; + let infinite_penumbra = Value { + asset_id: penumbra.id(), + amount: Amount::from(100000u128) * penumbra.unit_amount(), + }; + let infinite_pusd = Value { + asset_id: pusd.id(), + amount: Amount::from(100000u128) * pusd.unit_amount(), + }; + + state_tx.vcb_credit(infinite_gn).await?; + state_tx.vcb_credit(infinite_gm).await?; + state_tx.vcb_credit(infinite_penumbra).await?; + state_tx.vcb_credit(infinite_pusd).await?; + tracing::info!(gm_id = ?gm.id()); tracing::info!(gn_id = ?gn.id()); tracing::info!(pusd_id = ?pusd.id()); @@ -685,7 +727,9 @@ async fn swap_execution_tests() -> anyhow::Result<()> { // Set the batch swap flow for the trading pair. Arc::get_mut(&mut state) .unwrap() - .put_swap_flow(&trading_pair, swap_flow.clone()); + .put_swap_flow(&trading_pair, swap_flow.clone()) + .await + .unwrap(); let routing_params = state.routing_params().await.unwrap(); state .handle_batch_swaps(trading_pair, swap_flow, 0u32.into(), 0, routing_params) diff --git a/crates/core/component/dex/src/event.rs b/crates/core/component/dex/src/event.rs index 8b7c152a8e..660efc6be4 100644 --- a/crates/core/component/dex/src/event.rs +++ b/crates/core/component/dex/src/event.rs @@ -8,6 +8,8 @@ use crate::{ BatchSwapOutputData, SwapExecution, }; +use penumbra_asset::asset; +use penumbra_num::Amount; use penumbra_proto::penumbra::core::component::dex::v1 as pb; pub fn swap(swap: &Swap) -> pb::EventSwap { @@ -91,3 +93,27 @@ pub fn arb_execution(height: u64, swap_execution: SwapExecution) -> pb::EventArb swap_execution: Some(swap_execution.into()), } } + +pub fn vcb_credit( + asset_id: asset::Id, + previous_balance: Amount, + new_balance: Amount, +) -> pb::EventValueCircuitBreakerCredit { + pb::EventValueCircuitBreakerCredit { + asset_id: Some(asset_id.into()), + previous_balance: Some(previous_balance.into()), + new_balance: Some(new_balance.into()), + } +} + +pub fn vcb_debit( + asset_id: asset::Id, + previous_balance: Amount, + new_balance: Amount, +) -> pb::EventValueCircuitBreakerDebit { + pb::EventValueCircuitBreakerDebit { + asset_id: Some(asset_id.into()), + previous_balance: Some(previous_balance.into()), + new_balance: Some(new_balance.into()), + } +} diff --git a/crates/core/component/dex/src/lib.rs b/crates/core/component/dex/src/lib.rs index a7f77e9543..a330e0e971 100644 --- a/crates/core/component/dex/src/lib.rs +++ b/crates/core/component/dex/src/lib.rs @@ -8,13 +8,11 @@ pub mod genesis; pub mod state_key; mod batch_swap_output_data; -mod circuit_breaker; mod params; mod swap_execution; mod trading_pair; pub use batch_swap_output_data::BatchSwapOutputData; -pub(crate) use circuit_breaker::ExecutionCircuitBreaker; pub use params::DexParameters; pub use swap_execution::SwapExecution; pub use trading_pair::{DirectedTradingPair, DirectedUnitPair, TradingPair, TradingPairVar}; diff --git a/crates/core/component/dex/src/lp/position.rs b/crates/core/component/dex/src/lp/position.rs index b31878f2ab..e6aae9c636 100644 --- a/crates/core/component/dex/src/lp/position.rs +++ b/crates/core/component/dex/src/lp/position.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Context}; -use penumbra_asset::asset; +use penumbra_asset::{asset, Value}; use penumbra_num::Amount; use penumbra_proto::{ penumbra::core::component::dex::v1 as pb, serializers::bech32str, DomainType, @@ -156,6 +156,22 @@ impl Position { None } } + + /// Returns the amount of reserves for asset 1. + pub fn reserves_1(&self) -> Value { + Value { + amount: self.reserves.r1, + asset_id: self.phi.pair.asset_1(), + } + } + + /// Returns the amount of reserves for asset 2. + pub fn reserves_2(&self) -> Value { + Value { + amount: self.reserves.r2, + asset_id: self.phi.pair.asset_2(), + } + } } /// A hash of a [`Position`]. diff --git a/crates/core/component/dex/src/state_key.rs b/crates/core/component/dex/src/state_key.rs index cca51976c6..dad76e571e 100644 --- a/crates/core/component/dex/src/state_key.rs +++ b/crates/core/component/dex/src/state_key.rs @@ -1,5 +1,7 @@ use std::string::String; +use penumbra_asset::asset; + use crate::{lp::position, DirectedTradingPair, TradingPair}; pub mod config { @@ -12,6 +14,10 @@ pub mod config { } } +pub fn value_balance(asset_id: &asset::Id) -> String { + format!("dex/value_balance/{asset_id}") +} + pub fn positions(trading_pair: &TradingPair, position_id: &str) -> String { format!("dex/positions/{trading_pair}/opened/{position_id}") } diff --git a/crates/proto/src/gen/penumbra.core.component.dex.v1.rs b/crates/proto/src/gen/penumbra.core.component.dex.v1.rs index 9b8568691b..27e6a5c183 100644 --- a/crates/proto/src/gen/penumbra.core.component.dex.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.dex.v1.rs @@ -1444,6 +1444,48 @@ impl ::prost::Name for EventArbExecution { ::prost::alloc::format!("penumbra.core.component.dex.v1.{}", Self::NAME) } } +/// Indicates that value was added to the DEX. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EventValueCircuitBreakerCredit { + /// The asset ID being deposited into the DEX. + #[prost(message, optional, tag = "1")] + pub asset_id: ::core::option::Option, + /// The previous balance of the asset in the DEX. + #[prost(message, optional, tag = "2")] + pub previous_balance: ::core::option::Option, + /// The new balance of the asset in the DEX. + #[prost(message, optional, tag = "3")] + pub new_balance: ::core::option::Option, +} +impl ::prost::Name for EventValueCircuitBreakerCredit { + const NAME: &'static str = "EventValueCircuitBreakerCredit"; + const PACKAGE: &'static str = "penumbra.core.component.dex.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.core.component.dex.v1.{}", Self::NAME) + } +} +/// Indicates that value is leaving the DEX. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EventValueCircuitBreakerDebit { + /// The asset ID being deposited into the DEX. + #[prost(message, optional, tag = "1")] + pub asset_id: ::core::option::Option, + /// The previous balance of the asset in the DEX. + #[prost(message, optional, tag = "2")] + pub previous_balance: ::core::option::Option, + /// The new balance of the asset in the DEX. + #[prost(message, optional, tag = "3")] + pub new_balance: ::core::option::Option, +} +impl ::prost::Name for EventValueCircuitBreakerDebit { + const NAME: &'static str = "EventValueCircuitBreakerDebit"; + const PACKAGE: &'static str = "penumbra.core.component.dex.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.core.component.dex.v1.{}", Self::NAME) + } +} #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct DexParameters { diff --git a/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs index a9a2c86f01..14e166d4f8 100644 --- a/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs @@ -2414,6 +2414,270 @@ impl<'de> serde::Deserialize<'de> for EventSwapClaim { deserializer.deserialize_struct("penumbra.core.component.dex.v1.EventSwapClaim", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for EventValueCircuitBreakerCredit { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.asset_id.is_some() { + len += 1; + } + if self.previous_balance.is_some() { + len += 1; + } + if self.new_balance.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.dex.v1.EventValueCircuitBreakerCredit", len)?; + if let Some(v) = self.asset_id.as_ref() { + struct_ser.serialize_field("assetId", v)?; + } + if let Some(v) = self.previous_balance.as_ref() { + struct_ser.serialize_field("previousBalance", v)?; + } + if let Some(v) = self.new_balance.as_ref() { + struct_ser.serialize_field("newBalance", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for EventValueCircuitBreakerCredit { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "asset_id", + "assetId", + "previous_balance", + "previousBalance", + "new_balance", + "newBalance", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + AssetId, + PreviousBalance, + NewBalance, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "assetId" | "asset_id" => Ok(GeneratedField::AssetId), + "previousBalance" | "previous_balance" => Ok(GeneratedField::PreviousBalance), + "newBalance" | "new_balance" => Ok(GeneratedField::NewBalance), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = EventValueCircuitBreakerCredit; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.dex.v1.EventValueCircuitBreakerCredit") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut asset_id__ = None; + let mut previous_balance__ = None; + let mut new_balance__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::AssetId => { + if asset_id__.is_some() { + return Err(serde::de::Error::duplicate_field("assetId")); + } + asset_id__ = map_.next_value()?; + } + GeneratedField::PreviousBalance => { + if previous_balance__.is_some() { + return Err(serde::de::Error::duplicate_field("previousBalance")); + } + previous_balance__ = map_.next_value()?; + } + GeneratedField::NewBalance => { + if new_balance__.is_some() { + return Err(serde::de::Error::duplicate_field("newBalance")); + } + new_balance__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(EventValueCircuitBreakerCredit { + asset_id: asset_id__, + previous_balance: previous_balance__, + new_balance: new_balance__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.dex.v1.EventValueCircuitBreakerCredit", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for EventValueCircuitBreakerDebit { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.asset_id.is_some() { + len += 1; + } + if self.previous_balance.is_some() { + len += 1; + } + if self.new_balance.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.dex.v1.EventValueCircuitBreakerDebit", len)?; + if let Some(v) = self.asset_id.as_ref() { + struct_ser.serialize_field("assetId", v)?; + } + if let Some(v) = self.previous_balance.as_ref() { + struct_ser.serialize_field("previousBalance", v)?; + } + if let Some(v) = self.new_balance.as_ref() { + struct_ser.serialize_field("newBalance", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for EventValueCircuitBreakerDebit { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "asset_id", + "assetId", + "previous_balance", + "previousBalance", + "new_balance", + "newBalance", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + AssetId, + PreviousBalance, + NewBalance, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "assetId" | "asset_id" => Ok(GeneratedField::AssetId), + "previousBalance" | "previous_balance" => Ok(GeneratedField::PreviousBalance), + "newBalance" | "new_balance" => Ok(GeneratedField::NewBalance), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = EventValueCircuitBreakerDebit; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.dex.v1.EventValueCircuitBreakerDebit") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut asset_id__ = None; + let mut previous_balance__ = None; + let mut new_balance__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::AssetId => { + if asset_id__.is_some() { + return Err(serde::de::Error::duplicate_field("assetId")); + } + asset_id__ = map_.next_value()?; + } + GeneratedField::PreviousBalance => { + if previous_balance__.is_some() { + return Err(serde::de::Error::duplicate_field("previousBalance")); + } + previous_balance__ = map_.next_value()?; + } + GeneratedField::NewBalance => { + if new_balance__.is_some() { + return Err(serde::de::Error::duplicate_field("newBalance")); + } + new_balance__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(EventValueCircuitBreakerDebit { + asset_id: asset_id__, + previous_balance: previous_balance__, + new_balance: new_balance__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.dex.v1.EventValueCircuitBreakerDebit", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for GenesisContent { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index be786d18c9a755a867b7fad793fc56cba981deb5..1800672860b39d90c0e0575f4ffb28942245e087 100644 GIT binary patch delta 1374 zcmb_bO=uHA6wXd|(l}8Xw?Buf?b;f}Qd_kMwaxa>AEeNF67eS6X0)p%DcNm$3_`(M zFCO+Jlu|0hP$D6ONJ$a#>|F%0_2AWOMev*19jKsCiid&y=DqiQ@4at!{YU4)VQ1yA zq<{EGnEx&xy;{BU`sIz@ZyM{KF7T{(-(FhblTLnN#qlQc+vZP3Uos&x&RGue!@V0+v( zy+zwI3t}VYs_mL~Cc`tPm%}|br=}kaGL~RbeM~d-4I019MwzaUk%*yhQeuMW9@1>r z<(@e+WzKUa``FCz<(%typmDMxQ88dT!>A~Z$!ddz5Y(Hr?VN&90lRycC6y4|q9oNd zB$bo_X6TEVdn6kTI&iW_x(>)SXraE?8nSDWFKH?Wu`SwniH%4xV(8mqtbQp5Cy%W| zpo3B+Lm;zFlWHbXgwD1YY6L!{jaZ3B-oS_czx&|0<`TCHVwZoqkOpmu##7RTv~Zz( zJQs>b{S=!)F*-(d-~u41%gO~nG+b6Lq{TQpCw2klMTmhQSP!t|3ta$&*p6}`P2fW1 zBrc#5GALCtqV0-wVOWk#pi>b;orVjR5v$V3TdK+TZ6UQv;~nfK9)cw{Tl4#7wZUP0 zQvgb0^2q=qvqoFaDhSRRZSP>WWg&ufe{EK~HDM7=2cU$7nc^wdX(D(Nh>BEten3UR zQ>R1M*^K1D4jTT?S#A2RMF<*bP!bO_#Z(P?N;M#cYKR8JP~%2ym*US4h8joaE{%7x vVQ`L%p_=~soHmrgH=1dxO#vdaNt0bt0uY?d=lgRS4#A1y==J?MT7L2y0&zLJ delta 367 zcmXw!F-rqM5QT5&b~fjnqFItFRImynh*5)u+*N{wjhO4KZNxufCs=JIA%+X#l0vxR zet~_8)Pjh;c5+xaduEG)`ItA4_dcrV;X8V%a{2WNWSW(~SL{xoj*7>(Ull$t1T{2z z-W1LI*l!iH&z;-ODuIl(8lX2uK_U4v7FrH4XwxxC8H1^T@JHad!6yDn>%; diff --git a/proto/penumbra/penumbra/core/component/dex/v1/dex.proto b/proto/penumbra/penumbra/core/component/dex/v1/dex.proto index ffe0005c1a..b8468b1e0f 100644 --- a/proto/penumbra/penumbra/core/component/dex/v1/dex.proto +++ b/proto/penumbra/penumbra/core/component/dex/v1/dex.proto @@ -669,6 +669,26 @@ message EventArbExecution { SwapExecution swap_execution = 2; } +// Indicates that value was added to the DEX. +message EventValueCircuitBreakerCredit { + // The asset ID being deposited into the DEX. + asset.v1.AssetId asset_id = 1; + // The previous balance of the asset in the DEX. + num.v1.Amount previous_balance = 2; + // The new balance of the asset in the DEX. + num.v1.Amount new_balance = 3; +} + +// Indicates that value is leaving the DEX. +message EventValueCircuitBreakerDebit { + // The asset ID being deposited into the DEX. + asset.v1.AssetId asset_id = 1; + // The previous balance of the asset in the DEX. + num.v1.Amount previous_balance = 2; + // The new balance of the asset in the DEX. + num.v1.Amount new_balance = 3; +} + message DexParameters { // Whether or not the DEX is enabled. bool is_enabled = 1; From ae418b24c0034c9e647c3a3ad72af67713b59ae7 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Mon, 25 Mar 2024 12:48:04 -0400 Subject: [PATCH 008/168] penumbra: update link in readme (#4096) --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e61da2a315..366559e603 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,13 @@ To join the test network as a full node, follow setup instructions for Penumbra ## Current work and roadmap -For a high-level view of current work-in-progress and future items, check out our [project board][Board]. +For a high-level view of current work-in-progress and future items, check out our: -[Board]: https://github.com/orgs/penumbra-zone/projects/17 +- [Tracking issues][Tracking] +- [Backlog][Backlog] + +[Tracking]: https://github.com/orgs/penumbra-zone/projects/23/views/4 +[Backlog]: https://github.com/orgs/penumbra-zone/projects/23/views/1 [Discord]: https://discord.gg/hKvkrqa3zC [Penumbra]: https://penumbra.zone [protocol]: https://protocol.penumbra.zone From db3e2bc170f177f306125b1f42b2807db0f7e142 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Mon, 25 Mar 2024 13:28:00 -0400 Subject: [PATCH 009/168] cnidarium: use `WriteBatch` when writing jmt nodes (#4094) This PR is a prelude to implementing "staged" writes to our storage layer (cc @noot #4095 ). Specifically, it: - inline the `write_node_batch` logic inside of `SubstoreStorage::commit` - make sure that nodes are inserted into the `rocksdb::WriteBatch` rather than immediately written to disk The last point was a bug, we want to commit to a set of writes atomically, but the tree changes were written ahead of the rest of the transaction. By inserting a `panic!` statement between the tree write and the `write(node_batch` statement and restarting the full node I was able to observe a consistency discrepancy. This PR fixes this. --- crates/cnidarium/src/storage.rs | 85 +++++++++++++----------- crates/cnidarium/src/store/substore.rs | 90 +++++++++++++------------- 2 files changed, 91 insertions(+), 84 deletions(-) diff --git a/crates/cnidarium/src/storage.rs b/crates/cnidarium/src/storage.rs index 689ecd8838..adf8708bb1 100644 --- a/crates/cnidarium/src/storage.rs +++ b/crates/cnidarium/src/storage.rs @@ -1,7 +1,7 @@ use std::{path::PathBuf, sync::Arc}; // use tokio_stream::wrappers::WatchStream; -use anyhow::{bail, Result}; +use anyhow::{bail, ensure, Result}; use parking_lot::RwLock; use rocksdb::{Options, DB}; use tokio::sync::watch; @@ -40,7 +40,6 @@ struct Inner { changes_rx: watch::Receiver<(jmt::Version, Arc)>, snapshots: RwLock, multistore_config: MultistoreConfig, - #[allow(dead_code)] /// A handle to the dispatcher task. /// This is used by `Storage::release` to wait for the task to terminate. jh_dispatcher: Option>, @@ -277,17 +276,22 @@ impl Storage { pub async fn commit(&self, delta: StateDelta) -> Result { // Extract the snapshot and the changes from the state delta let (snapshot, changes) = delta.flatten(); + let prev_snapshot_version = snapshot.version(); // We use wrapping_add here so that we can write `new_version = 0` by // overflowing `PRE_GENESIS_VERSION`. - let old_version = self.latest_version(); - let new_version = old_version.wrapping_add(1); - tracing::debug!(old_version, new_version); - if old_version != snapshot.version() { - anyhow::bail!("version mismatch in commit: expected state forked from version {} but found state forked from version {}", old_version, snapshot.version()); - } + let prev_storage_version = self.latest_version(); + let next_storage_version = prev_storage_version.wrapping_add(1); + tracing::debug!(prev_storage_version, next_storage_version); + + ensure!( + prev_storage_version == prev_snapshot_version, + "trying to commit a delta forked from version {}, but the latest version is {}", + prev_snapshot_version, + prev_storage_version + ); - self.commit_inner(snapshot, changes, new_version, false) + self.commit_inner(snapshot, changes, next_storage_version, false) .await } @@ -377,7 +381,7 @@ impl Storage { let substore_storage = SubstoreStorage { substore_snapshot }; - // Commit the substore and collect the root hash + // Commit the substore and collect its root hash let (root_hash, substore_batch) = substore_storage .commit(changeset, write_batch, version, perform_migration) .await?; @@ -392,7 +396,7 @@ impl Storage { substore_roots.push((config.clone(), root_hash, version)); } - /* commit roots to main store */ + // Add substore roots to the main store changeset let main_store_config = self.0.multistore_config.main_store.clone(); let mut main_store_changes = changes_by_substore .remove(&main_store_config) @@ -407,7 +411,7 @@ impl Storage { .insert(config.prefix.to_string(), Some(root_hash.0.to_vec())); } - /* commit main substore */ + // Commit the main store and collect the global root hash let main_store_snapshot = SubstoreSnapshot { config: main_store_config.clone(), rocksdb_snapshot: snapshot.0.snapshot.clone(), @@ -427,9 +431,15 @@ impl Storage { ?version, "added main store to write batch" ); + db.write(write_batch).expect("can write to db"); + tracing::debug!( + ?global_root_hash, + ?version, + "committed main store and substores to db" + ); - /* update multistore versions */ + // Update the tracked versions for each substore. for (config, root_hash, new_version) in substore_roots { tracing::debug!( ?root_hash, @@ -443,38 +453,37 @@ impl Storage { tracing::debug!(?global_root_hash, ?version, "updating main store version"); multistore_versions.set_version(main_store_config, version); - /* hydrate the snapshot cache */ - if perform_migration { + // If we're not performing a migration, we should update the snapshot cache + if !perform_migration { + tracing::debug!("updating snapshot cache"); + + let latest_snapshot = Snapshot::new(db.clone(), version, multistore_versions); + // Obtain a write lock to the snapshot cache, and push the latest snapshot + // available. The lock guard is implicitly dropped immediately. + self.0 + .snapshots + .write() + .try_push(latest_snapshot.clone()) + .expect("should process snapshots with consecutive jmt versions"); + + tracing::debug!(?version, "dispatching snapshot"); + + // Send fails if the channel is closed (i.e., if there are no receivers); + // in this case, we should ignore the error, we have no one to notify. + let _ = self + .0 + .dispatcher_tx + .send((latest_snapshot, (version, changes))); + } else { tracing::debug!("skipping snapshot cache update"); - return Ok(global_root_hash); } - tracing::debug!("updating snapshot cache"); - - let latest_snapshot = Snapshot::new(db.clone(), version, multistore_versions); - // Obtain a write lock to the snapshot cache, and push the latest snapshot - // available. The lock guard is implicitly dropped immediately. - self.0 - .snapshots - .write() - .try_push(latest_snapshot.clone()) - .expect("should process snapshots with consecutive jmt versions"); - - tracing::debug!(?version, "dispatching snapshot"); - - // Send fails if the channel is closed (i.e., if there are no receivers); - // in this case, we should ignore the error, we have no one to notify. - let _ = self - .0 - .dispatcher_tx - .send((latest_snapshot, (version, changes))); - Ok(global_root_hash) } #[cfg(feature = "migration")] - /// Commits the provided [`StateDelta`] to persistent storage without increasing the version - /// of the chain state. + /// Commit the provided [`StateDelta`] to persistent storage without increasing the version + /// of the chain state, and skips the snapshot cache update. pub async fn commit_in_place(&self, delta: StateDelta) -> Result { let (snapshot, changes) = delta.flatten(); let old_version = self.latest_version(); diff --git a/crates/cnidarium/src/store/substore.rs b/crates/cnidarium/src/store/substore.rs index 670878a863..ad9eadfdf9 100644 --- a/crates/cnidarium/src/store/substore.rs +++ b/crates/cnidarium/src/store/substore.rs @@ -375,8 +375,11 @@ impl SubstoreStorage { .into_iter() .map(|(key, some_value)| (KeyHash::with::(&key), key, some_value)) .collect(); + let cf_jmt_keys = self.substore_snapshot.config.cf_jmt_keys(&self.substore_snapshot.db); let cf_jmt_keys_by_keyhash = self.substore_snapshot.config.cf_jmt_keys_by_keyhash(&self.substore_snapshot.db); + let cf_jmt = self.substore_snapshot.config.cf_jmt(&self.substore_snapshot.db); + let cf_jmt_values = self.substore_snapshot.config.cf_jmt_values(&self.substore_snapshot.db); /* Keyhash and pre-image indices */ for (keyhash, key_preimage, value) in unwritten_changes.iter() { @@ -403,8 +406,24 @@ impl SubstoreStorage { jmt.put_value_set(unwritten_changes.into_iter().map(skip_key), write_version)? }; - self.write_node_batch(&batch.node_batch)?; - tracing::trace!(?root_hash, "wrote node batch to backing store"); + /* JMT nodes and values */ + for (node_key, node) in batch.node_batch.nodes() { + let db_node_key_bytes= DbNodeKey::encode_from_node_key(node_key)?; + let value_bytes = borsh::to_vec(node)?; + tracing::trace!(?db_node_key_bytes, value_bytes = ?hex::encode(&value_bytes)); + write_batch.put_cf(cf_jmt, db_node_key_bytes, value_bytes); + } + + + for ((version, key_hash), some_value) in batch.node_batch.values() { + let key_bytes = VersionedKeyHash::encode_from_keyhash(key_hash, version); + let value_bytes = borsh::to_vec(some_value)?; + tracing::trace!(?key_bytes, value_bytes = ?hex::encode(&value_bytes)); + write_batch.put_cf(cf_jmt_values, key_bytes, value_bytes); + } + + tracing::trace!(?root_hash, "accumulated node changes in the write batch"); + for (k, v) in cache.nonverifiable_changes.into_iter() { let cf_nonverifiable = self.substore_snapshot.config.cf_nonverifiable(&self.substore_snapshot.db); @@ -418,6 +437,7 @@ impl SubstoreStorage { } }; } + Ok((root_hash, write_batch)) }) })? @@ -426,48 +446,22 @@ impl SubstoreStorage { } impl TreeWriter for SubstoreStorage { - /// Writes a [`NodeBatch`] into storage which includes the JMT - /// nodes (`DbNodeKey` -> `Node`) and the JMT values, - /// (`VersionedKeyHash` -> `Option>`). - fn write_node_batch(&self, node_batch: &jmt::storage::NodeBatch) -> Result<()> { - let node_batch = node_batch.clone(); - let cf_jmt = self - .substore_snapshot - .config - .cf_jmt(&self.substore_snapshot.db); - - for (node_key, node) in node_batch.nodes() { - let db_node_key = DbNodeKey::from(node_key.clone()); - let db_node_key_bytes = db_node_key.encode()?; - let value_bytes = borsh::to_vec(node)?; - tracing::trace!(?db_node_key_bytes, value_bytes = ?hex::encode(&value_bytes)); - self.substore_snapshot - .db - .put_cf(cf_jmt, db_node_key_bytes, value_bytes)?; - } - let cf_jmt_values = self - .substore_snapshot - .config - .cf_jmt_values(&self.substore_snapshot.db); - - for ((version, key_hash), some_value) in node_batch.values() { - let versioned_key = VersionedKeyHash::new(*version, *key_hash); - let key_bytes = &versioned_key.encode(); - let value_bytes = borsh::to_vec(some_value)?; - tracing::trace!(?key_bytes, value_bytes = ?hex::encode(&value_bytes)); - - self.substore_snapshot - .db - .put_cf(cf_jmt_values, key_bytes, value_bytes)?; - } - - Ok(()) + fn write_node_batch(&self, _node_batch: &jmt::storage::NodeBatch) -> Result<()> { + // The "write"-part of the `TreeReader + TreeWriter` jmt architecture does not work + // well with a deferred write strategy. + // What we would like to do is to accumulate the changes in a write batch, and then commit + // them all at once. This isn't possible to do easily because the `TreeWriter` trait + // rightfully does not expose RocksDB-specific types in its API. + // + // The alternative is to use interior mutability but the semantics become + // so implementation specific that we lose the benefits of the trait abstraction. + unimplemented!("We inline the tree writing logic in the `commit` method") } } /// An ordered node key is a node key that is encoded in a way that /// preserves the order of the node keys in the database. -pub struct DbNodeKey(NodeKey); +pub struct DbNodeKey(pub NodeKey); impl DbNodeKey { pub fn from(node_key: NodeKey) -> Self { @@ -479,9 +473,13 @@ impl DbNodeKey { } pub fn encode(&self) -> Result> { + Self::encode_from_node_key(&self.0) + } + + pub fn encode_from_node_key(node_key: &NodeKey) -> Result> { let mut bytes = Vec::new(); - bytes.extend_from_slice(&self.0.version().to_be_bytes()); // encode version as big-endian - let rest = borsh::to_vec(&self.0)?; + bytes.extend_from_slice(&node_key.version().to_be_bytes()); // encode version as big-endian + let rest = borsh::to_vec(node_key)?; bytes.extend_from_slice(&rest); Ok(bytes) } @@ -506,13 +504,13 @@ pub struct VersionedKeyHash { } impl VersionedKeyHash { - pub fn new(version: jmt::Version, key_hash: KeyHash) -> Self { - Self { version, key_hash } + pub fn encode(&self) -> Vec { + VersionedKeyHash::encode_from_keyhash(&self.key_hash, &self.version) } - pub fn encode(&self) -> Vec { - let mut buf: Vec = self.key_hash.0.to_vec(); - buf.extend_from_slice(&self.version.to_be_bytes()); + pub fn encode_from_keyhash(key_hash: &KeyHash, version: &jmt::Version) -> Vec { + let mut buf: Vec = key_hash.0.to_vec(); + buf.extend_from_slice(&version.to_be_bytes()); buf } From c8094a88c226e0b31a0aafa14ad81b754e7bd54c Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Mon, 25 Mar 2024 17:34:05 -0400 Subject: [PATCH 010/168] =?UTF-8?q?refactor(staking):=20=F0=9F=8C=B9=20hoi?= =?UTF-8?q?st=20uptime=20tracking=20into=20`ValidatorUptimeTracker`=20exte?= =?UTF-8?q?nsion=20trait=20(#4099)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit this is a small collection of noop refactors, cherry-picked out of #4070. most importantly, this hoists `track_uptime` into a standalone extension trait. additionally, some improvements to telemetry helped make debugging tests in #4070 easier. --- .../component/stake/src/component/stake.rs | 22 +- .../stake/src/component/stake/address.rs | 23 +++ .../stake/src/component/validator_handler.rs | 3 + .../validator_handler/uptime_tracker.rs | 189 ++++++++++++++++++ .../validator_handler/validator_manager.rs | 99 +-------- 5 files changed, 223 insertions(+), 113 deletions(-) create mode 100644 crates/core/component/stake/src/component/stake/address.rs create mode 100644 crates/core/component/stake/src/component/validator_handler/uptime_tracker.rs diff --git a/crates/core/component/stake/src/component/stake.rs b/crates/core/component/stake/src/component/stake.rs index d3c06d2052..6871000caf 100644 --- a/crates/core/component/stake/src/component/stake.rs +++ b/crates/core/component/stake/src/component/stake.rs @@ -1,3 +1,5 @@ +pub mod address; + use crate::params::StakeParameters; use crate::rate::BaseRateData; use crate::validator::{self, Validator}; @@ -14,7 +16,6 @@ use futures::{StreamExt, TryStreamExt}; use penumbra_num::Amount; use penumbra_proto::{StateReadProto, StateWriteProto}; use penumbra_sct::component::clock::EpochRead; -use sha2::{Digest, Sha256}; use std::pin::Pin; use std::str::FromStr; use std::{collections::BTreeMap, sync::Arc}; @@ -25,7 +26,9 @@ use tendermint::{block, PublicKey}; use tracing::{error, instrument, trace}; use crate::component::epoch_handler::EpochHandler; -use crate::component::validator_handler::{ValidatorDataRead, ValidatorManager}; +use crate::component::validator_handler::{ + ValidatorDataRead, ValidatorManager, ValidatorUptimeTracker, +}; pub struct Staking {} @@ -323,20 +326,7 @@ pub trait StateWriteExt: StateWrite { identity_key: &IdentityKey, consensus_key: &PublicKey, ) { - /// Translates from consensus keys to the truncated sha256 hashes in last_commit_info - /// This should really be a refined type upstream, but we can't currently upstream - /// to tendermint-rs, for process reasons, and shouldn't do our own tendermint data - /// modeling, so this is an interim hack. - fn validator_address(ck: &PublicKey) -> [u8; 20] { - let ck_bytes = ck.to_bytes(); - let addr: [u8; 20] = Sha256::digest(ck_bytes).as_slice()[0..20] - .try_into() - .expect("Sha256 digest should be 20-bytes long"); - - addr - } - - let address = validator_address(consensus_key); + let address = self::address::validator_address(consensus_key); tracing::debug!(?identity_key, ?consensus_key, hash = ?hex::encode(address), "registering consensus key"); self.put( state_key::validators::lookup_by::cometbft_address(&address), diff --git a/crates/core/component/stake/src/component/stake/address.rs b/crates/core/component/stake/src/component/stake/address.rs new file mode 100644 index 0000000000..70c9458c53 --- /dev/null +++ b/crates/core/component/stake/src/component/stake/address.rs @@ -0,0 +1,23 @@ +use { + sha2::{Digest, Sha256}, + tendermint::PublicKey, +}; + +/// A type alias for 20-byte truncated SHA256 validator addresses. +/// +/// This is the format in which [`tendermint::abci::types::CommitInfo`] presents vote information. +pub(crate) type Address = [u8; ADDRESS_LEN]; + +const ADDRESS_LEN: usize = 20; + +/// Translates from consensus keys to the truncated sha256 hashes in `last_commit_info`. +// +// NOTE: This should really be a refined type upstream, but we can't currently upstream to +// tendermint-rs, for process reasons, and shouldn't do our own tendermint data modeling, so +// this is an interim hack. +pub(crate) fn validator_address(ck: &PublicKey) -> Address { + let ck_bytes = ck.to_bytes(); + Sha256::digest(ck_bytes).as_slice()[0..ADDRESS_LEN] + .try_into() + .expect("Sha256 digest should be 20-bytes long") +} diff --git a/crates/core/component/stake/src/component/validator_handler.rs b/crates/core/component/stake/src/component/validator_handler.rs index 05e268cf53..f5e5081a5f 100644 --- a/crates/core/component/stake/src/component/validator_handler.rs +++ b/crates/core/component/stake/src/component/validator_handler.rs @@ -5,3 +5,6 @@ pub mod validator_store; pub use validator_store::ValidatorDataRead; pub(crate) use validator_store::ValidatorDataWrite; pub(crate) use validator_store::ValidatorPoolTracker; + +pub mod uptime_tracker; +pub use uptime_tracker::ValidatorUptimeTracker; diff --git a/crates/core/component/stake/src/component/validator_handler/uptime_tracker.rs b/crates/core/component/stake/src/component/validator_handler/uptime_tracker.rs new file mode 100644 index 0000000000..3c3190a98c --- /dev/null +++ b/crates/core/component/stake/src/component/validator_handler/uptime_tracker.rs @@ -0,0 +1,189 @@ +use { + super::{ValidatorDataRead, ValidatorDataWrite, ValidatorManager}, + crate::{ + component::{ + metrics, + stake::{ + address::{validator_address, Address}, + ConsensusIndexRead, + }, + StateReadExt as _, + }, + params::StakeParameters, + validator, IdentityKey, Uptime, + }, + anyhow::Result, + async_trait::async_trait, + cnidarium::StateWrite, + futures::StreamExt as _, + penumbra_sct::component::clock::EpochRead, + std::collections::BTreeMap, + tap::Tap, + tendermint::abci::types::CommitInfo, + tokio::task::{AbortHandle, JoinSet}, + tracing::{debug, error_span, instrument, trace, Instrument}, +}; + +/// A bundle of information about a validator used to track its uptime. +type ValidatorInformation = (IdentityKey, tendermint::PublicKey, Uptime); + +/// A collection of tasks retrieving [`ValidatorInformation`]. +type Lookups = JoinSet>>; + +/// Tracks validator uptimes. +/// +/// Use [`track_uptime()`] to process a block's [`CommitInfo`] and update validator uptime +/// bookkeeping. +/// +/// [`track_uptime()`]: Self::track_uptime +#[async_trait] +pub trait ValidatorUptimeTracker: StateWrite { + #[instrument(skip(self, last_commit_info))] + async fn track_uptime(&mut self, last_commit_info: &CommitInfo) -> Result<()> { + // Note: this probably isn't the correct height for the LastCommitInfo, + // which is about the *last* commit, but at least it'll be consistent, + // which is all we need to count signatures. + let height = self.get_block_height().await?; + let params = self.get_stake_params().await?; + + // Build a mapping from addresses (20-byte truncated SHA256(pubkey)) to vote statuses. + let did_address_vote = last_commit_info + .votes + .as_slice() + .tap(|votes| { + if votes.is_empty() { + debug!("no validators voted") + } else { + debug!(len = %votes.len(), "collecting validator votes") + } + }) + .into_iter() + .map(|vote| (vote.validator.address, vote.sig_info.is_signed())) + .inspect(|(address, voted)| { + trace!( + address = %hex::encode(address), + %voted, + "validator vote information" + ) + }) + .collect::>(); + + // Since we don't have a lookup from "addresses" to identity keys, + // iterate over our app's validators, and match them up with the vote data. + // We can fetch all the data required for processing each validator concurrently: + let mut lookups = Lookups::new(); + let mut validator_identity_stream = self.consensus_set_stream()?; + while let Some(identity_key) = validator_identity_stream.next().await.transpose()? { + self.spawn_validator_lookup_fut(identity_key, &mut lookups); + } + + // Now process the data we fetched concurrently. + // Note that this will process validator uptime changes in a random order, but because they are all + // independent, this doesn't introduce any nondeterminism into the complete state change. + while let Some(data) = lookups.join_next().await.transpose()? { + if let Some(validator_info) = data? { + self.process_vote(validator_info, &did_address_vote, ¶ms, height) + .await?; + } + } + + Ok(()) + } + + /// Spawns a future that will retrieve validator information. + /// + /// NB: This function is synchronous, but the lookup will run asynchronously as part of the + /// provided [`JoinSet`]. This permits us to fetch information about all of the validators + /// in the consensus set in parallel. + /// + /// # Panics + /// + /// This will panic if there is no recorded state for a validator with the given + /// [`IdentityKey`]. + fn spawn_validator_lookup_fut( + &self, + identity_key: crate::IdentityKey, + lookups: &mut Lookups, + ) -> AbortHandle { + // Define, but do not yet `.await` upon, a collection of futures fetching information + // about a validator. + let state = self.get_validator_state(&identity_key); + let uptime = self.get_validator_uptime(&identity_key); + let consensus_key = self.fetch_validator_consensus_key(&identity_key); + + // Define a span indicating that the spawned future follows from the current context. + let span = { + let span = error_span!("fetching validator information", %identity_key); + let current = tracing::Span::current(); + span.follows_from(current); + span + }; + + lookups.spawn( + async move { + let state = state + .await? + .expect("every known validator must have a recorded state"); + + match state { + validator::State::Active => { + // If the validator is active, we need its consensus key and current uptime data: + Ok(Some(( + identity_key, + consensus_key + .await? + .expect("every known validator must have a recorded consensus key"), + uptime + .await? + .expect("every known validator must have a recorded uptime"), + ))) + } + _ => { + // Otherwise, we don't need to track its uptime, and there's no data to fetch. + Ok(None) + } + } + } + .instrument(span), + ) + } + + async fn process_vote( + &mut self, + (identity_key, consensus_key, mut uptime): ValidatorInformation, + did_address_vote: &BTreeMap, + params: &StakeParameters, + height: u64, + ) -> anyhow::Result<()> { + let addr = validator_address(&consensus_key); + let voted = did_address_vote + .get(&addr) + .cloned() + // If the height is `1`, then the `LastCommitInfo` refers to the genesis block, + // which has no signers -- so we'll mark all validators as having signed. + // https://github.com/penumbra-zone/penumbra/issues/1050 + .unwrap_or(height == 1); + + tracing::debug!( + ?voted, + num_missed_blocks = ?uptime.num_missed_blocks(), + ?identity_key, + ?params.missed_blocks_maximum, + "recorded vote info" + ); + metrics::gauge!(metrics::MISSED_BLOCKS, "identity_key" => identity_key.to_string()) + .increment(uptime.num_missed_blocks() as f64); + + uptime.mark_height_as_signed(height, voted)?; + if uptime.num_missed_blocks() as u64 >= params.missed_blocks_maximum { + self.set_validator_state(&identity_key, validator::State::Jailed) + .await?; + } else { + self.set_validator_uptime(&identity_key, uptime); + } + + Ok(()) + } +} + +impl ValidatorUptimeTracker for T {} diff --git a/crates/core/component/stake/src/component/validator_handler/validator_manager.rs b/crates/core/component/stake/src/component/validator_handler/validator_manager.rs index 58780c9012..710f2f04f1 100644 --- a/crates/core/component/stake/src/component/validator_handler/validator_manager.rs +++ b/crates/core/component/stake/src/component/validator_handler/validator_manager.rs @@ -2,7 +2,7 @@ use { crate::{ component::{ metrics, - stake::{ConsensusIndexRead, ConsensusIndexWrite, RateDataWrite}, + stake::{ConsensusIndexWrite, RateDataWrite}, validator_handler::{ validator_store::ValidatorPoolTracker, ValidatorDataRead, ValidatorDataWrite, }, @@ -16,17 +16,14 @@ use { anyhow::{ensure, Result}, async_trait::async_trait, cnidarium::StateWrite, - futures::StreamExt as _, penumbra_asset::asset, penumbra_num::Amount, penumbra_proto::StateWriteProto, penumbra_sct::component::clock::{EpochManager, EpochRead}, penumbra_sct::component::StateReadExt as _, penumbra_shielded_pool::component::AssetRegistry, - sha2::{Digest as _, Sha256}, std::collections::BTreeMap, - tendermint::abci::types::{CommitInfo, Misbehavior}, - tokio::task::JoinSet, + tendermint::abci::types::Misbehavior, tracing::{instrument, Instrument}, }; @@ -39,7 +36,6 @@ use { /// ## Validator management /// - Add validator definition via [`add_validator`]. /// - Update validator definitions via [`update_validator_definition`]. -/// - Tracking a validator's uptime via [`track_uptime`]. /// - Process byzantine behavior evidence via [`process_evidence`]. /// /// ## State machine interface @@ -82,7 +78,6 @@ use { /// [`update_validator_definition`]: Self::update_validator_definition /// [`set_validator_state`]: Self::set_validator_state /// [`try_precursor_transition`]: Self::try_precursor_transition -/// [`track_uptime`]: Self::track_uptime /// [`process_evidence`]: Self::process_evidence pub trait ValidatorManager: StateWrite { /// Execute a legal state transition, updating the validator records and @@ -637,96 +632,6 @@ pub trait ValidatorManager: StateWrite { Ok(()) } - #[instrument(skip(self, last_commit_info))] - async fn track_uptime(&mut self, last_commit_info: &CommitInfo) -> Result<()> { - // Note: this probably isn't the correct height for the LastCommitInfo, - // which is about the *last* commit, but at least it'll be consistent, - // which is all we need to count signatures. - let height = self.get_block_height().await?; - let params = self.get_stake_params().await?; - - // Build a mapping from addresses (20-byte truncated SHA256(pubkey)) to vote statuses. - let did_address_vote = last_commit_info - .votes - .iter() - .map(|vote| (vote.validator.address, vote.sig_info.is_signed())) - .collect::>(); - - // Since we don't have a lookup from "addresses" to identity keys, - // iterate over our app's validators, and match them up with the vote data. - // We can fetch all the data required for processing each validator concurrently: - let mut js = JoinSet::new(); - let mut validator_identity_stream = self.consensus_set_stream()?; - while let Some(identity_key) = validator_identity_stream.next().await { - let identity_key = identity_key?; - let state = self.get_validator_state(&identity_key); - let uptime = self.get_validator_uptime(&identity_key); - let consensus_key = self.fetch_validator_consensus_key(&identity_key); - js.spawn(async move { - let state = state - .await? - .expect("every known validator must have a recorded state"); - - match state { - validator::State::Active => { - // If the validator is active, we need its consensus key and current uptime data: - Ok(Some(( - identity_key, - consensus_key - .await? - .expect("every known validator must have a recorded consensus key"), - uptime - .await? - .expect("every known validator must have a recorded uptime"), - ))) - } - _ => { - // Otherwise, we don't need to track its uptime, and there's no data to fetch. - anyhow::Ok(None) - } - } - }); - } - // Now process the data we fetched concurrently. - // Note that this will process validator uptime changes in a random order, but because they are all - // independent, this doesn't introduce any nondeterminism into the complete state change. - while let Some(data) = js.join_next().await.transpose()? { - if let Some((identity_key, consensus_key, mut uptime)) = data? { - // for some reason last_commit_info has truncated sha256 hashes - let addr: [u8; 20] = - Sha256::digest(&consensus_key.to_bytes()).as_slice()[0..20].try_into()?; - - let voted = did_address_vote - .get(&addr) - .cloned() - // If the height is `1`, then the `LastCommitInfo` refers to the genesis block, - // which has no signers -- so we'll mark all validators as having signed. - // https://github.com/penumbra-zone/penumbra/issues/1050 - .unwrap_or(height == 1); - - tracing::debug!( - ?voted, - num_missed_blocks = ?uptime.num_missed_blocks(), - ?identity_key, - ?params.missed_blocks_maximum, - "recorded vote info" - ); - metrics::gauge!(metrics::MISSED_BLOCKS, "identity_key" => identity_key.to_string()) - .increment(uptime.num_missed_blocks() as f64); - - uptime.mark_height_as_signed(height, voted)?; - if uptime.num_missed_blocks() as u64 >= params.missed_blocks_maximum { - self.set_validator_state(&identity_key, validator::State::Jailed) - .await?; - } else { - self.set_validator_uptime(&identity_key, uptime); - } - } - } - - Ok(()) - } - /// Process evidence of byzantine behavior from CometBFT. /// /// Evidence *MUST* be processed before `end_block` is called, because From 8f55a1849c57ff8f1fcb189d9bd4ccf2cae440e7 Mon Sep 17 00:00:00 2001 From: aubrey Date: Mon, 25 Mar 2024 14:57:07 -0700 Subject: [PATCH 011/168] compact block: add epoch index to compact block --- .../src/gen/proto_descriptor.bin.no_lfs | Bin 98383 -> 88631 bytes .../compact-block/src/compact_block.rs | 5 +++++ .../compact-block/src/component/manager.rs | 8 +++++++ ...enumbra.core.component.compact_block.v1.rs | 3 +++ ...a.core.component.compact_block.v1.serde.rs | 21 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 376163 -> 366417 bytes .../compact_block/v1/compact_block.proto | 2 ++ 7 files changed, 39 insertions(+) diff --git a/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs b/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs index 67d781308dc15349cc1633e0986b9be4d49fda1f..c2c1f4df906c5e549e4bf4ff1514fc587b551e16 100644 GIT binary patch delta 16472 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 WQ+w4Mx1hSdx_-eQmpW|U1OEvTzRX+z delta 26297 zcmbVU3wTw?!`wD)EbcrB3LYz()+J@?0vAdx7P3TBWum9 zf7Yy7GqYyRoc+Yl%-<}uOy{$^4JWws?H1PRZzo#!Rq1(d)7Hi&@_xj-9D* zXlrP0Dlhc3a|?%*51iH9JgYG?u%)%Rt$D`WnFFgtdHZQLS~s3EoU&lQRTf-lZ611| z6PuYyx6N(MWXp#@Ix(cn-$;)Z6((ldCWMs(*SuM$&8_5^K=A zj<2emR5dE8yRK6>uJW>~akUewuDW{61n^k<6N#9iz0khB=tKR&f3fZ0`toZ7tMp3k zllDoa7inj}{AzMMYSrlvZDXX3j#!>j+};hmuTz%6ym8`F(* zGvy2t@d<)x-&($3tHDUQ$&!jkH7CEODbqZ&riDD`Os%yqr!}mYue}l!b@|B|V_|=3 zltdibnMtc+5excty=X?RC8;O>taG~OI{EdP8FOdVwr1+nb#3KjxY$MJ%yeTmGaY_%TT&~5!0=IHq)AmHKc&1Zk6 z^*j}KoM`&FcS6gV4VlLJ+7?XMOjBK^TnmmQ%l&YM4jfIz<7Me(!*18KRhqF%)BZ~< zaAQ?d#*Z5_a?GUi4x7_su9Woqf)2|M+7k+RLkh{A>BKWlbLZ5y&2JHIRXC(qxKj9P zp^0=IScTe?Q2DZ+TnCHmkZ`S$<7y^Wjp7=_U881vRdxCM_JkOLQ+4-V&FS2lX+e)N z^|hk2=xB3&L(?oij)gO6=Tvheu zlfF?qenQpgF;loDCKSnbl!&{)Nqj5Y+*CWWxphvut=!_Ro}*fg8qM*)v8X?qSHJ86 z-k`op6Jy`mb;ng*UODocwU<>*oK#yidUVYM;faoGR*oB2GnsntP=f-X;##MGk3#5# zDW}uuF;(M6k=7Hdsz+AUUR`}fbTqbetjsj1EZf_CIWNzErr z;Xy!|=oEc!?A@+T`IS`@CstlwRr>`BaqFDn4bZH>iI1upH|EMQw3)MtZ#Yggv~Fjo zWb8!ff+4)Ja#F1?hJiX4cf?o%p_B6k1D<4kt=K_3`GJjpKd_UB=-)f5li#ZzYsKET zlLzgJ)68)rKMwcRDNZchk2Ss@4_BV}MgFhk21Zb4?yM6+IJeR%n$ytKFbC_K+|A3y z?Ebr<)PzDQz%l*iCXSK5=?n}pOaBpu5RiJMZquCdzo+d#j047>8RtcTz`weKw$Jwd zo%wwaX^t(GqbXE1YRu<6ums_XWR9OuGpS}svcpErGnp48-?!ajCtuYZGI+=Z7-w=K z#X7%s60Hd})#JVi$B_j-dHax@66SVuIxCT3`0v7;!tXD&K3_aEr}#hrws=@h@qYC>$pK56hPE@oc?){nW*sgg_-i~IhUXv%CC_B)>f4Z5cx%zN3-?_*!%siIo>X)!isR3T4*V34-%gkALw| zc79V^`X;YJ=~FS#BaO2e5AnkQ>|>cw)&Aa$xozHz%L&6!D6O z`HX4D15mV?ZS%5oTUr|D3yC3vxZnj;i$~#PBFFb;r0Z^&mu{`kdKj%1?8P%08XMZ? zhc?o<8IsaO8cZ~*q?42276MVlI?E!4id9TiWK@QVKhbzi-POlplpfYRIRUchR=Ume zxq)V6GEH7e>d*W4t`0=v!k<~6{Z(v9=d^Rr~zwuU*G0nM%O5LAQ#d=G8TcSPS?8?af19oNyH zv8+(`PDg*n7MD5y&bwo{dNoU>TDHhr+uZeYWhB=0z(PW++TsYw3p_3W!DzVU?$q7D1i2~+i zY=tlbQE;otXQiCJTo^@Dg7pLX51kSUYY?6i3X>~Oad{UZtU-86*KW>NxG=Pv78DNr z+R$mCFoW>4P?+3%8g_osGK286?w)fM7Y1=%uwdZCq1T1NI)txF@Y#U2$d|4w=B6f! z>i6#Be1{9em!=2ypanyxhr$Mgr-#Bc*wc$W*|GuQ={vM;T?**`fS!y){FyXc8C%O%It)4oH$TscUIkt17&uP3g;87RWOU=>P^9= z=bsqX6iPdgZb~T6aUk84>MPrEAl)=zpz{MEopANGpn1qTzb%wbV3gWIM@m4t%`KPG z2}rkf>E;aJP3OD%Jm#hNwvxN%(-y#&Z@%n{WdNGyql@#xX7Zuryo4)(y?nGYuSEGl zKH8brt-EtBrXO?7f6Kc+`0WMf`u`T|%twIH)LX#1aA6PtULb)%LH`9(e^9_(Ag3TG z;4Z)vB#&Y=M{Z`qFdzVOa|ohAyPM@OqKs;Ky@lI=1j^0lo+nW-7k%jrj4`12Aqx4d zAEHouzH|oE1c(o3$mgo)uCYjC{hR{$EQ$3Od8U5(wL>7rQoVNBz@h*O=e zjz#4d2ICgZ=vI;MjcKZDoSSX9F@xbQa%@qGwo#lPF-&3)_N^HY`rYl)URPmw@c zqJ@rux_4=85HIu@!@5Vbg2KWF5*{X?uq%S}`YIAocSTCsu5q`4gLs)t0ffpoX#_Ui2*cSx?GxmKXD6ah(huAb%r=j;RU6u_)UbX12R9i*&Syn(-hZ_@LJP0^TA>H1VMJwPAe5>onsZ*4P#~<3XeL!94wITzhntL(}dkXx&joWLrx_KgZv{s$ zE(=CpJfjMv7%8exBehN|z_}A^31@JD(ryt0i0d@3hXehDZkWf928S+QTlHuz65=A` z(Oe|*nn$%#vE~poR$?%)=De(sz<4ZZ9`Vu`T8JOZMMIzjMnQ3?1`WhxnwyFkB&>Bi z+gYN}fUzm~?j^VTv=(p5MMLBSMh+UqnwxUbFzjFetrs>1C<`ZgLNkgg$`E(YX}%GG zcS9D5Zjmu{_>xAoFicNq1%*nfIE~{I{uvxti&uG!yi+HmY}452)PHFn5^!GTrnXFL zQ@WAD{MPJ6-l!U{x@MAh^~9=y-efx3WW0tZe%8ZAUx#EPMg7e<(slJN9x8lomy%DdFLLXo;p#l6M)2Vup0FAlvp_Q}&!s2c4dkXNeF4zhiamMD|Tkh!;Ooy0b*a}V6E z_2}tbN^KjMh0kd0xnRqsUoU<}v&)^4&n+IgRHhk zW1q;i3p611Xm-Mx2(y5swQFy%XynzC_A2XfE}-`+>k$KDuPN(sS}=t5I0F)|k@eC8eP{7{#}9B`A0RBa*Nn@pdTTqX!tg zcQENfOo|e~D2Nykc}GhYhmBzL-_7ka?U7(aea1l>4D@*ylV-va_W?vwo0vB#J!?RH zrmi_qiMH;HRS(6?%w3!)#6e;OOX1NV$jw#LG0LXEb8c-q?`l zOV;$yU#U!26I=$bG#IoOLyu_K<5;lc8|PIV%hewDFvc;`URA?#dmPnzR}};Ccy85VR6Q;_ig|-A(K?~A)8V|P z)$D|3=Od3o5+_OGCEB?O z8H~R`B=wZWbG@yZZ_jO@R9s_vMy3%vFOqjiSf(2ro9BV+V^jE?Ya)9Fx0I<&+W1sH z(ZT-KCyRZsrLm!|p-rScXCs@5i%%h>YW5c{D=tHkx#c&%p~Hqgj(ytadDvOVMYqj{ zZakga(A*mOH2Q-sqeD@bMGw(s3U>{%61&%RWdfF$smodbZ0s*Nokcb192`!-VO4eRd?P`~-j4&)#|eWW z69%%Pr-LIr^#rT={Vb@fxn;!t5rhqh`=gH*so=*m+U2yU_>f6K^6>TLiq))$ZJ`-I2}P0 zNNcG(?QthYSXhsrZ2!?!n~V`pMi>sXc`_PYB*Q=^6$Y|lAaTN~KwM$E)-PDzj$q9- zJ^WuV#1*8XNt5Df<$s`{B1oYkNWed>Itvo;Ppe4*67Wx}Ibo7-ZdaWJ1tgFP2^lAl zva`KW%xlZuf&|od>TMrrYPr_4EbpzLcJev?vkduY($peNcPdRm0UxBm2MJAgDosHG zey7qDB;a={O)b*&IVSTLpnwEYA(5ufu|!WkXg-R0?Oq}P1PQ3;m`q?;G<~0ENOO`O zg8~#t1x3^Mc_u@4lmh8_hD@id45a57Qk_2`r?=>I`U+!vn4bDi=1rg|)oTuJo!To{ zH{~z>E6nM_QxB*|mUxwYVP1-tLJ1U$(@+y;gDb9c@uQHjJA_gk# zW;`z?GG9T@sqRQ{B2Zf7cISeUopv*siQ*Ul-AzVcPqWFQ)#>%%rm2q(e?77gEl#g9 z8Em5pFkWZfM4}KRiLil#oJJ0Se(ejUPiq>nF9M*IXYvfPY89g9Q9L%B?^G{vG93HW}?*wJCrC z5=eza_Ig*XJU)tfZMiFe1k}4~SFp*g-eXK8E5NCbB@{TYq)yW%`$=0rcB zO2Gz)nM~-#xD7xmD6+v}CKGxn1=3+A6M7(lbeJKZHyAdEyVl1n@2{{y#Thmjdwut* zA2TGGsJS@V;Fz)jC}<8+GzStkIHqg>63rb`HUNp{j*$&klBdT>nG?Y?*RS?ZLG^=H45UvP5=TI~ z3dkT&GS*v`(peXzf}-hnk|o5k6mphSL!|MGdDT zc#0ZMNAM=#Pe<@3;7=3$PYU4#j%z)v=dCi@znvanCm+^hCE|w3p>baqY@B^!#5!Ht z1w6F7PM6UNNVL07cg4aD678Z4)^iRdQ!}Dkbu~%pCi@?km&tp{k)5~ zaY3T@oAs{@bI#-VsJk@?=JdOEYXndEiLDVl4gOX=DKZ-lAN;NQ;2{!^5*=Ih3r6vi z6G|!2*{XkKq|+g60O#1zo@$z{UAH4@fxKf!)B-KeJM^To0eQy`{XDVQ^R)_O{}Hl5 zEY~0}9PW&x4p92zS)Dx}>~FrJ_*va9aYo?~12zrc&fwyf(G@#&BhF8LoC|~Q)bk6R z%Vjk*{amoNWlY6$x;#QsH98YLr{h4cs)7DhQ26aJ{;zbIDxhk#9REs3a)D+i-pIJd z3p%@4DoWQMAjOnIA*R#|x(swcqTv_xaxn!!qR%hrebk_l%f1i|8oBHXG-&TQ!m7Y} zrTxIS>$MTDL@iJl{7U3m6b8Sd%az38Vel(@_a4q3m^02a7eP=Mye}FU z3WN7Wo<(8szR0s|D7i0E(k4VwQWyh;Z|O{p8r>m)RK}oS`7POw4`nD?eoIe^)d(aS zdP_gIue4;0WAz)ZNHp}8{j;8l-B7ZW{JS9imuzf7Bt0rT6<$kK$-( zf7GKm8rmQA2!ZwiohdCGGz3yLL;?4Ks38it59moXycBRB(EEs35o3sU4(NS_r*s01 zh7Raoy~sIE`-H=gJLw3%HSdp8JE8_~KW%hG4d8+rWKtZIAb|!tbX*0#s>6x_vCzQ5>;(bg;#$~X15-~0rx#oteC_7DE&-{a zTtM9tLAijs#lY_Xgh^dM-D30`;QUfRmEeBWxYvKiUlJjd0AWdlPy&P{M!21o0AYy% zgV5E0!%A_OH~!VD0f=M1K;9W9^()01w$zYU11Lt@OAUE701|C4rM4IFw#$$PFz#JA zzp@M{%c2gH0c4pW@AVy429RZjyyXK4kYxsL`S8e8H)w|5X9Q0!yxqUgkQsM}XWT)G zj5|oI==T}OxYKQ;gQaDq!C#lb0Cc;w(nu5t{rTNeE(t+S(xoNdwy!br4jJOolBVh! zBi6}z%|vGsJWSZo{_LVR*t89*E8s%C4Tjv*K|*MQkrW}Ylrazj)8K-{1l(ZAJrN`( z;06PG;vZN$giktbG35G`;6ce21Bo1=KLsUQjP8RK3H&Wa z-$C5!30VC}gO%{*JwaA~(n#csn%wFU>BnijI8B>Qf|mCujY5&q0`)nP*F~wQ%PHJb zaE*9u{-hC+s`Md?TNLJb#b9TrmRWQmScRLxW;|(ZA9QKFHn*;ef%$r(SON?#gPxLrZ7*pp~{1h8>8sC9DHRQmpEbg|-KbVPca4i6J{+46osV z9+A9wL^WR>5;Xen{xhq?6nZY(@cElV$U{{CDAg!hdRMIuD1~b8s?`A`+J2W-2YLpO zpd(+0LC*jx@i%ZfA{=@t(xDazlme$iEf64q(?JUaUCYr1`$2Hz?rX<q8^&KP)lJX~+D~h?P3`lIim){yg3u+_PNsk1KV+ggVC!nE?g~ znd62`+k%8T#|<18$t6L;;>V3HBJc)@NpjpcN4!$Xr@;3UgDo<3QJdfUePZB#pgSU4 z6q5oc45KJ@X%SAh{57#Rr@3A|RN^(zJgSJ7OPqo3oiK2LzCxEEsByZzW7#V0o2QWv z;9)dXr1rkTW2S{K?q+Bd(+kZ|L1-&6yRNyZt+lz);WrC8*92tk zQTaS40hNRYf`mKVVaoLwBv&{oGVCBRRCkydsx`Ex=TrYznC&;*cR%x2L>T0mD@?iB zqY^Myn0Qqv#+MFiD@-KUB#Gf(VRjP-D^$jCuQ1O&A0|cvn48v0lRXp;H|ev|vTf?HPg?uoy|eHbr+a;r@oX3pb-#jk`OFoR#O>f7^y+1kU~WC??1NPAuLZpC=*IT;%xcY^wms@Pt%Tbn^U=b#-ITWr`FthZZsK-9be#^7 z+s(83DiU4aZX&Zv@6ushOzEA$j)xwbwll(~wQFaDPixmsQzp0}0iAZ5h@%8PtzA1! znP5OE@OPTX02~zffO;|b^}~mzz8K+ChT+8spBA7OO}U-r^9ATdQ*LJHt_I0j4oZ#i4cx~mhmm_#OtGyh-)4KF>WFA_VUN$?6eIuV)fPdLciOnA*;9oX- z_Z3;Cd~%dmgAHp}U;k=^PY1bIBYZl@y=o?vtIaN;Bv2-hIj+SX|w`{xQ*uwc*<)g-m;5jf=__g zb0x@&!H6Wt2Vc*X0R4t(h!<7)2A%@aBmzggC!j{i9p4~dqHR5&B8z<{p0|pw(YX_( zn$+}mZJ&vE*Rm8?`^+xNO=wHoXX2E$flk@^a3bqnlN~Yj)C0Jbr{$@A<_~*l{=25# zS#}9e$z0?2rqNX{jpC&l7~--S7>xFe-ur+e5c8KJ{OLR*QTEAQv3MH3K=NG<$U0*oBBV5x)A*yucG2ZkCOFd%Ud zW8;U7z{x(y6`@%IMkIpYGJp{}fKChIq1?Wc<1>1pCUPc#qM@^ZBM=8LLnlcX$5<_#;#95aw>~y8UH z%+JT9;-oSY-s_`Ors`1&tP>_)?2EZcDU%cCfI&RI%%_ydNmHCY{V)GA4f1Fk0IKZ- z+CFK@(^BM9br%ZNHs**p4XbHZt{<2H)s-Kt&56}8R%7Z8* zfOpE22T_o~J7vl^lQJ!THR<_R;QiS2|7yy!Xg&`%L5g4#Bz*3#CW6hbh)(n2af>Wg zP*D^L5 zLU>HcgaTSMZ?jm>)FA&~d1wfEm>&Sb{2*ce+blVpAko-umK;uyF#l~9^73?CC?MZo zVyWQ_p4@y{C3wICQ{mBt)DlY$CrY`998QqHTVl!Kq{PR~z*EF|MBw}OL@rY~8pR00$2EVpDl3KAmA zt)#f?0|}LuTX<^m9_`tM)WX`7v`sx?v6|FY7WBBQ z!xWD#OMd!IA6#L4)Pb6hL~2rB9LMdR@ThhHi0%pB(FG_oEAor>%B=_2&+zS+>KnnK)3Da&+?p5SkKeh6nmF`7} z&YxPba_3WWjw14}O;&L7u|N5nlulrxp-sx_AR)6!IR{ASv`IM!NLYQ7at@Gij!ono zG`Wjtz_$D|M$h$s)*`t50@#gCHVQ~ODn;JZNOITkwb1*o`h1gw_SM>NRA;F zD$0I8L!RUpktaQ)JP8z}KnhO+2@^h}JSpi~yR5u7r6*CwdzXb&CJl6w9O-%MOQ(d# zO5;HuodJO83`lh5c{R`=(b)59c7a4^p0}_mzfKv1B$;+k`+}eC)vhOh*%QIgGPg&K z0qRjj<;frcv&WJ*B_J^ddn|cV0up1e$HGksZ52scuJ&RIU-@;K%zL9ID30B$%#Lbk zVy`kgNHnomO<=k+c!Tc`NizEz%Iu&Z1yYzDBpQ1|nZ4Mx_E~v<728u18~&hkIAXuNP--**l|nT6oba7fFb+`Bu3?! zCF5?8X!@AdRpie=!UD%Ed2tOA7C3HM;z|o7EO6W^?&WjWE~bW0e0kTFo(*wm26?3! z`O69A*(il(CzNM{gk~qmv*C0$x(V_m-#G9ma85=z{9T6fJCp+Fr1CqEB&ht3E}Blk z@2(wuO1WGycR7&4CwAtNu zyYuuI&A-!j#M^2VkVD*M8x{O?!f7ZBhB!2E2K2bgmOp9Y3@Ckq;0w72+ zIv_DR_t=RNzAS?T);%_UDMhO^NFd!~BTdI2)4Dtt9oWG&JHGD+5e8`#*fQ1Ua^DMV zc{K3fq@g zg{Tdv6_K@3d8N(7-7E|SWRMDqGHWX%r6^~x(k@m*L#h3hav!8Z z@+ph9D$Zq_t2RP^)Y__Ii`NyTr8~vGe|HYpFjYnSc=REpEd8WuWWRpi zEdHITjCU?QS8r)<6Z3kA4K^@XuMy^E31jlsf$WOMDLl z38FdpHCI-|8<{jdC_*2FpwFeqkAF~{k;M~ryqxFRY5YDcJKI4df+su<)`n&@REbZy(5DbX73o_X4pc3jrL^5Z{QhhP5D74##>NAl!I3GG8au-STBTylU$@egcU{1^lz&Fdf9 zG7rXSQ~*OHhd2Xs?T5BZ4iN(t)?tz53q=Y2x`(z2aK!4uYd~b3U6kbO5ivmjC=?+Y z5@19UqDGDcJnz44L;Ti~5NPIsArdE?0nGolJNcAHG>j7T!}_t!9=8p?=zzwY0Yi8? zNX&yD+wwggNWlHrmOr@#iRON6<8rtN3l>N`?)l3Xzw8R!k%EaA>{DW-ol-a9KpQ;4+7mT!Pj9SZESdkmF^ zrhRUgeerNk1m0wtlc(yJv21M1YjsTQwX`+mRVQ$KXq!i zEx*G65{BMw<7W)Qqj56_w1KaOhStf@dm=;Q!7SYMe3q$iCV(p7o zV9<~NL#@DIKw_V7;|QWez<3MMq#RAm8;F1*E>fuxGW1(E|CuLwG_*cov-d(nQ<4V^ zWoSzB9I)kG6KVtPfZau0;(>&n57<6_!ypVzFY*uAgWn4cZIGeg4m*tp7i8#nB13}# z$af<{EkcKwioK4Dhe%mVD3NM7pt)}mNm5YtlMXNW}U+jjV1UVZpkIq~HLvIGUu;WzORRHr@p z@j)3`hhOM(5Q6g$$0E~*s1{*;rj=*=X0|rZQNboY>n%Rh;L~>+C?{C|`NtyJOa{NC zR&jgohv6ncm53IxJCCZHXM8JzAa_Ov{h%Mycv&7bL(_C?9in{IkGe)g@%Zo`J}>Ln z&88%y{ER|>RFOZbogqI6h~D64hSH7nAd%M1MX^}Q`6->tOX*yGYmD6y(^C(U{pO@w zJl;(TzUQC$T}Gj28sA!M`1ah4vYsE>6hVDMTNd9wlph`(=vCv3eK=ky<#4u1Q-Dv= zd|SGK4hB4&_qaEty;=AOQur0f{^F610`u?@K^noFo{NtAvge-q+8-)nw4-lfh_46o z^9ViT;U^;;vv@97d_EBx;Ru&)Yo!B5noc(K*<=Ce!Hlv6nVEsjV8dHuxCo{7$p=lF z%WW|k9CI4$4HzPl;S8)ex5ecBDrW%WwwSzME)B1m8@ ziS-g+@Bs;=C9y$+oyC;iD<$pjjU%Fnv;FHz#iiIG*qaxOlDDZX!Yo=A#w zN1vC@k{7_{bL;4b+tKSRHFBaFebte^`^Y~ZseC~V1UHEfHB=YKB6r8|X*62dpd(kZ zL?jVFq6>G&kU_asbO8&=z0`%9L=VJAWk2Wg|G)M=sUN;Ab3E~(g)h(^pL=YeuPH?H zL#a)1EHnv8^y1#A7i8*tV_`2SjJ=n7@rH)=ugtZqHL<+=?xzi%WGoDtuHRd zAeGU0?~1iAefHmD84ze?mudwhTGYi$^Qeohjp}BSE9~BXOtHxkrQ1KvPTW;&2V$ z8^f>Mu;5c#4S6M=h6=j>uF2>1pam+5esc!;y)G_Yl{0|7E-qb_GoZ`5xO7#{fS0U` zOIH;PIK{d+yrf2|1B~@?qgU$dIqC%Y`-WXf)yY#)8tFi};PuM(yatyq70rl%lEqRNRW^NYQ!i zsW{FRLqjQWw#9>A9vDQfAoFtD;#ML@iVV9gjyL}_aZrmU^wV*f3gNV1M1x1iwx{C- oA}PgdK$oZExNoM3!x^y4({WratgdrPXEo1iZfS3M`?;I`59fS(@Bjb+ diff --git a/crates/core/component/compact-block/src/compact_block.rs b/crates/core/component/compact-block/src/compact_block.rs index 4a896c708f..2041a0995f 100644 --- a/crates/core/component/compact-block/src/compact_block.rs +++ b/crates/core/component/compact-block/src/compact_block.rs @@ -38,6 +38,8 @@ pub struct CompactBlock { pub app_parameters_updated: bool, /// Updated gas prices, if they have changed. pub gas_prices: Option, + // The epoch index + pub epoch_index: u64, // **IMPORTANT NOTE FOR FUTURE HUMANS**: if you want to add new fields to the `CompactBlock`, // you must update `CompactBlock::requires_scanning` to check for the emptiness of those fields, // because the client will skip processing any compact block that is marked as not requiring @@ -57,6 +59,7 @@ impl Default for CompactBlock { swap_outputs: BTreeMap::new(), app_parameters_updated: false, gas_prices: None, + epoch_index: 0, } } } @@ -95,6 +98,7 @@ impl From for pb::CompactBlock { swap_outputs: cb.swap_outputs.into_values().map(Into::into).collect(), app_parameters_updated: cb.app_parameters_updated, gas_prices: cb.gas_prices.map(Into::into), + epoch_index: cb.epoch_index, } } } @@ -132,6 +136,7 @@ impl TryFrom for CompactBlock { proposal_started: value.proposal_started, app_parameters_updated: value.app_parameters_updated, gas_prices: value.gas_prices.map(TryInto::try_into).transpose()?, + epoch_index: value.epoch_index, }) } } diff --git a/crates/core/component/compact-block/src/component/manager.rs b/crates/core/component/compact-block/src/component/manager.rs index f0bf34e122..3b84fabb45 100644 --- a/crates/core/component/compact-block/src/component/manager.rs +++ b/crates/core/component/compact-block/src/component/manager.rs @@ -113,6 +113,13 @@ trait Inner: StateWrite { // Add all the pending nullifiers to the compact block let nullifiers = self.pending_nullifiers().into_iter().collect(); + //Get the index of the current epoch + let epoch_index = self + .get_current_epoch() + .await + .expect("epoch is always set") + .index; + let compact_block = CompactBlock { height, state_payloads, @@ -124,6 +131,7 @@ trait Inner: StateWrite { fmd_parameters, app_parameters_updated, gas_prices, + epoch_index, }; self.nonverifiable_put_raw( diff --git a/crates/proto/src/gen/penumbra.core.component.compact_block.v1.rs b/crates/proto/src/gen/penumbra.core.component.compact_block.v1.rs index 5013cd3a27..92edba517e 100644 --- a/crates/proto/src/gen/penumbra.core.component.compact_block.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.compact_block.v1.rs @@ -39,6 +39,9 @@ pub struct CompactBlock { /// Updated gas prices, if they have changed. #[prost(message, optional, tag = "10")] pub gas_prices: ::core::option::Option, + /// The epoch index + #[prost(uint64, tag = "11")] + pub epoch_index: u64, } impl ::prost::Name for CompactBlock { const NAME: &'static str = "CompactBlock"; diff --git a/crates/proto/src/gen/penumbra.core.component.compact_block.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.compact_block.v1.serde.rs index 524801fad4..8f49ca36c7 100644 --- a/crates/proto/src/gen/penumbra.core.component.compact_block.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.compact_block.v1.serde.rs @@ -36,6 +36,9 @@ impl serde::Serialize for CompactBlock { if self.gas_prices.is_some() { len += 1; } + if self.epoch_index != 0 { + len += 1; + } let mut struct_ser = serializer.serialize_struct("penumbra.core.component.compact_block.v1.CompactBlock", len)?; if self.height != 0 { #[allow(clippy::needless_borrow)] @@ -68,6 +71,10 @@ impl serde::Serialize for CompactBlock { if let Some(v) = self.gas_prices.as_ref() { struct_ser.serialize_field("gasPrices", v)?; } + if self.epoch_index != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("epochIndex", ToString::to_string(&self.epoch_index).as_str())?; + } struct_ser.end() } } @@ -96,6 +103,8 @@ impl<'de> serde::Deserialize<'de> for CompactBlock { "appParametersUpdated", "gas_prices", "gasPrices", + "epoch_index", + "epochIndex", ]; #[allow(clippy::enum_variant_names)] @@ -110,6 +119,7 @@ impl<'de> serde::Deserialize<'de> for CompactBlock { SwapOutputs, AppParametersUpdated, GasPrices, + EpochIndex, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -142,6 +152,7 @@ impl<'de> serde::Deserialize<'de> for CompactBlock { "swapOutputs" | "swap_outputs" => Ok(GeneratedField::SwapOutputs), "appParametersUpdated" | "app_parameters_updated" => Ok(GeneratedField::AppParametersUpdated), "gasPrices" | "gas_prices" => Ok(GeneratedField::GasPrices), + "epochIndex" | "epoch_index" => Ok(GeneratedField::EpochIndex), _ => Ok(GeneratedField::__SkipField__), } } @@ -171,6 +182,7 @@ impl<'de> serde::Deserialize<'de> for CompactBlock { let mut swap_outputs__ = None; let mut app_parameters_updated__ = None; let mut gas_prices__ = None; + let mut epoch_index__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Height => { @@ -235,6 +247,14 @@ impl<'de> serde::Deserialize<'de> for CompactBlock { } gas_prices__ = map_.next_value()?; } + GeneratedField::EpochIndex => { + if epoch_index__.is_some() { + return Err(serde::de::Error::duplicate_field("epochIndex")); + } + epoch_index__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -251,6 +271,7 @@ impl<'de> serde::Deserialize<'de> for CompactBlock { swap_outputs: swap_outputs__.unwrap_or_default(), app_parameters_updated: app_parameters_updated__.unwrap_or_default(), gas_prices: gas_prices__, + epoch_index: epoch_index__.unwrap_or_default(), }) } } diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 1800672860b39d90c0e0575f4ffb28942245e087..4657e8905a4e08c32fd1486e6e53667eff6d5c50 100644 GIT binary patch delta 18135 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 a(D2G`15{`Ng73Wf6SzQ}(1)&B!he2u68 delta 27920 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 Date: Tue, 26 Mar 2024 08:22:37 -0400 Subject: [PATCH 012/168] =?UTF-8?q?tests(app):=20=F0=9F=A7=AA=20test=20tha?= =?UTF-8?q?t=20uptime=20tracks=20signed=20blocks=20(#4070)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **NB:** this is based on #4099. this adds test coverage, complementary to the work in #4061, which asserts that we properly track the _affirmative_ case of validators signing blocks. fixes #4040. see #3995. --- Cargo.lock | 1 + ...me_for_genesis_validator_missing_blocks.rs | 15 ++- ...me_for_genesis_validator_signing_blocks.rs | 78 +++++++++++ ..._uptime_for_validators_only_once_active.rs | 4 +- crates/test/mock-consensus/Cargo.toml | 1 + crates/test/mock-consensus/src/abci.rs | 8 +- crates/test/mock-consensus/src/block.rs | 24 +++- .../mock-consensus/src/block/signature.rs | 124 ++++++++++++++++++ 8 files changed, 238 insertions(+), 17 deletions(-) create mode 100644 crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs create mode 100644 crates/test/mock-consensus/src/block/signature.rs diff --git a/Cargo.lock b/Cargo.lock index 47f8156a7e..be1b12c5b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5282,6 +5282,7 @@ dependencies = [ "bytes", "ed25519-consensus", "rand_core 0.6.4", + "sha2 0.10.8", "tap", "tendermint", "tower", diff --git a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs index 285687d7c7..711e79ea17 100644 --- a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs +++ b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs @@ -11,7 +11,7 @@ use { component::validator_handler::validator_store::ValidatorDataRead, validator::Validator, }, tap::Tap, - tracing::{error_span, info, Instrument}, + tracing::{error_span, info, trace, Instrument}, }; #[tokio::test] @@ -60,10 +60,15 @@ async fn app_tracks_uptime_for_genesis_validator_missing_blocks() -> anyhow::Res // Jump ahead a few blocks. let height = 4; - node.fast_forward(height) - .instrument(error_span!("fast forwarding test node {height} blocks")) - .await - .context("fast forwarding {height} blocks")?; + for i in 1..=height { + node.block() + .with_signatures(Default::default()) + .execute() + .tap(|_| trace!(%i, "executing block with no signatures")) + .instrument(error_span!("executing block with no signatures", %i)) + .await + .context("executing block with no signatures")?; + } // Check the validator's uptime once more. We should have uptime data up to the fourth block, // and the validator should have missed all of the blocks between genesis and now. diff --git a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs new file mode 100644 index 0000000000..da23d60847 --- /dev/null +++ b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs @@ -0,0 +1,78 @@ +mod common; + +use { + self::common::BuilderExt, + anyhow::Context, + cnidarium::TempStorage, + penumbra_app::server::consensus::Consensus, + penumbra_genesis::AppState, + penumbra_mock_consensus::TestNode, + penumbra_stake::{ + component::validator_handler::validator_store::ValidatorDataRead, validator::Validator, + }, + tap::Tap, + tracing::{error_span, info, Instrument}, +}; + +#[tokio::test] +async fn app_tracks_uptime_for_genesis_validator_missing_blocks() -> anyhow::Result<()> { + // Install a test logger, acquire some temporary storage, and start the test node. + let guard = common::set_tracing_subscriber(); + let storage = TempStorage::new().await?; + + // Start the test node. + let mut node = { + let app_state = AppState::default(); + let consensus = Consensus::new(storage.as_ref().clone()); + TestNode::builder() + .single_validator() + .with_penumbra_auto_app_state(app_state)? + .init_chain(consensus) + .await + }?; + + // Retrieve the validator definition from the latest snapshot. + let Validator { identity_key, .. } = match storage + .latest_snapshot() + .validator_definitions() + .tap(|_| info!("getting validator definitions")) + .await? + .as_slice() + { + [v] => v.clone(), + unexpected => panic!("there should be one validator, got: {unexpected:?}"), + }; + let get_uptime = || async { + storage + .latest_snapshot() + .get_validator_uptime(&identity_key) + .await + .expect("should be able to get a validator uptime") + .expect("validator uptime should exist") + }; + + // Jump ahead a few blocks. + // TODO TODO TODO have the validator sign blocks here. + let height = 4; + node.fast_forward(height) + .instrument(error_span!("fast forwarding test node {height} blocks")) + .await + .context("fast forwarding {height} blocks")?; + + // Check the validator's uptime once more. We should have uptime data up to the fourth block, + // and the validator should have missed all of the blocks between genesis and now. + { + let uptime = get_uptime().await; + assert_eq!(uptime.as_of_height(), height); + assert_eq!( + uptime.num_missed_blocks(), + 0, + "validator should have signed the last {height} blocks" + ); + } + + Ok(()) + .tap(|_| drop(node)) + .tap(|_| drop(storage)) + .tap(|_| drop(guard)) +} diff --git a/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs b/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs index 839aaaa431..39656ddbff 100644 --- a/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs +++ b/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs @@ -287,8 +287,8 @@ async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<( ); assert_eq!( existing.num_missed_blocks(), - (EPOCH_DURATION - 1) as usize, - "genesis validator has missed all blocks in the previous epoch" + 0, + "genesis validator has signed all blocks in the previous epoch" ); } diff --git a/crates/test/mock-consensus/Cargo.toml b/crates/test/mock-consensus/Cargo.toml index 4cec69ae7e..9a844fad02 100644 --- a/crates/test/mock-consensus/Cargo.toml +++ b/crates/test/mock-consensus/Cargo.toml @@ -16,6 +16,7 @@ anyhow = { workspace = true } bytes = { workspace = true } ed25519-consensus = { workspace = true } rand_core = { workspace = true } +sha2 = { workspace = true } tap = { workspace = true } tendermint = { workspace = true } tower = { workspace = true, features = ["full"] } diff --git a/crates/test/mock-consensus/src/abci.rs b/crates/test/mock-consensus/src/abci.rs index aa490fc604..fd5c67f6bb 100644 --- a/crates/test/mock-consensus/src/abci.rs +++ b/crates/test/mock-consensus/src/abci.rs @@ -7,7 +7,7 @@ use { tap::{Tap, TapFallible}, tendermint::{ abci::types::CommitInfo, - block::{Header, Round}, + block::Header, v0_37::abci::{request, response, ConsensusRequest, ConsensusResponse}, }, tower::{BoxError, Service}, @@ -41,14 +41,12 @@ where pub async fn begin_block( &mut self, header: Header, + last_commit_info: CommitInfo, ) -> Result { let request = ConsensusRequest::BeginBlock(request::BeginBlock { hash: tendermint::Hash::None, header, - last_commit_info: CommitInfo { - round: Round::from(1_u8), - votes: Default::default(), - }, + last_commit_info, byzantine_validators: Default::default(), }); let service = self.service().await?; diff --git a/crates/test/mock-consensus/src/block.rs b/crates/test/mock-consensus/src/block.rs index c013927060..34a1e02bc8 100644 --- a/crates/test/mock-consensus/src/block.rs +++ b/crates/test/mock-consensus/src/block.rs @@ -2,6 +2,9 @@ //! //! Builders are acquired by calling [`TestNode::block()`]. +/// Interfaces for generating commit signatures. +mod signature; + use { crate::TestNode, tap::Tap, @@ -28,15 +31,22 @@ pub struct Builder<'e, C> { /// Evidence of malfeasance. evidence: evidence::List, + + /// The list of signatures. + signatures: Vec, } +// === impl TestNode === + impl TestNode { /// Returns a new [`Builder`]. pub fn block<'e>(&'e mut self) -> Builder<'e, C> { + let signatures = self.generate_signatures().collect(); Builder { test_node: self, data: Default::default(), evidence: Default::default(), + signatures, } } } @@ -60,8 +70,10 @@ impl<'e, C> Builder<'e, C> { Self { evidence, ..self } } - // TODO(kate): add more `with_` setters for fields in the header. - // TODO(kate): set some fields using state in the test node. + /// Sets the [`CommitSig`][block::CommitSig] commit signatures for this block. + pub fn with_signatures(self, signatures: Vec) -> Self { + Self { signatures, ..self } + } } impl<'e, C> Builder<'e, C> @@ -84,16 +96,17 @@ where header, data, evidence: _, - last_commit: _, + last_commit, .. } = block.tap(|block| { tracing::span::Span::current() .record("height", block.header.height.value()) .record("time", block.header.time.unix_timestamp()); }); + let last_commit_info = Self::last_commit_info(last_commit); trace!("sending block"); - test_node.begin_block(header).await?; + test_node.begin_block(header, last_commit_info).await?; for tx in data { let tx = tx.into(); test_node.deliver_tx(tx).await?; @@ -117,6 +130,7 @@ where data, evidence, test_node, + signatures, } = self; let height = { @@ -135,7 +149,7 @@ where height, round: Round::default(), block_id, - signatures: Vec::default(), + signatures, }) } else { None // The first block has no previous commit to speak of. diff --git a/crates/test/mock-consensus/src/block/signature.rs b/crates/test/mock-consensus/src/block/signature.rs new file mode 100644 index 0000000000..3275ddc433 --- /dev/null +++ b/crates/test/mock-consensus/src/block/signature.rs @@ -0,0 +1,124 @@ +use { + super::Builder, + crate::TestNode, + sha2::{Digest, Sha256}, + tendermint::{ + abci::types::{BlockSignatureInfo, CommitInfo, VoteInfo}, + account, + block::{BlockIdFlag, Commit, CommitSig, Round}, + vote::Power, + }, +}; + +/// Helper functions for generating [commit signatures]. +mod sign { + use tendermint::{account::Id, block::CommitSig, time::Time}; + + /// Returns a [commit signature] saying this validator voted for the block. + /// + /// [commit signature]: CommitSig + pub(super) fn commit(validator_address: Id) -> CommitSig { + CommitSig::BlockIdFlagCommit { + validator_address, + timestamp: timestamp(), + signature: None, + } + } + + /// Returns a [commit signature] saying this validator voted nil. + /// + /// [commit signature]: CommitSig + #[allow(dead_code)] + pub(super) fn nil(validator_address: Id) -> CommitSig { + CommitSig::BlockIdFlagNil { + validator_address, + timestamp: timestamp(), + signature: None, + } + } + + /// Generates a new timestamp, marked at the current time. + // + // TODO(kate): see https://github.com/penumbra-zone/penumbra/issues/3759, re: timestamps. + // eventually, we will add hooks so that we can control these timestamps. + fn timestamp() -> Time { + Time::now() + } +} + +// === impl TestNode === + +impl TestNode { + // TODO(kate): other interfaces may be helpful to add in the future, and these may eventually + // warrant being made `pub`. we defer doing so for now, only defining what is needed to provide + // commit signatures from all of the validators. + + /// Returns an [`Iterator`] of signatures for validators in the keyring. + pub(super) fn generate_signatures(&self) -> impl Iterator + '_ { + self.keyring + .iter() + // Compute the address of this validator. + .map(|(vk, _)| -> [u8; 20] { + ::digest(vk).as_slice()[0..20] + .try_into() + .expect("") + }) + .map(account::Id::new) + .map(self::sign::commit) + } +} + +// === impl Builder === + +impl<'e, C: 'e> Builder<'e, C> { + /// Returns [`CommitInfo`] given a block's [`Commit`]. + pub(super) fn last_commit_info(last_commit: Option) -> CommitInfo { + let Some(Commit { + round, signatures, .. + }) = last_commit + else { + // If there is no commit information about the last block, return an empty object. + return CommitInfo { + round: Round::default(), + votes: Vec::default(), + }; + }; + + CommitInfo { + round, + votes: signatures + .into_iter() + .map(Self::vote) + .filter_map(|v| v) + .collect(), + } + } + + /// Returns a [`VoteInfo`] for this [`CommitSig`]. + /// + /// If no validator voted, returns [`None`]. + fn vote(commit_sig: CommitSig) -> Option { + use tendermint::abci::types::Validator; + + // TODO(kate): upstream this into the `tendermint` library. + let sig_info = BlockSignatureInfo::Flag(match commit_sig { + CommitSig::BlockIdFlagAbsent => BlockIdFlag::Absent, + CommitSig::BlockIdFlagCommit { .. } => BlockIdFlag::Commit, + CommitSig::BlockIdFlagNil { .. } => BlockIdFlag::Nil, + }); + + let address: [u8; 20] = commit_sig + .validator_address()? + // TODO(kate): upstream an accessor to retrieve this as the [u8; 20] that it is. + .as_bytes() + .try_into() + .expect("validator address should be 20 bytes"); + let power = Power::from(1_u8); // TODO(kate): for now, hard-code voting power to 1. + let validator = Validator { address, power }; + + Some(VoteInfo { + validator, + sig_info, + }) + } +} From a6cb02428d35c3a520302d8ee86c08ae5e2ee8f1 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Tue, 26 Mar 2024 09:09:11 -0400 Subject: [PATCH 013/168] =?UTF-8?q?staking:=20=F0=9F=8F=81=20ex=20post=20f?= =?UTF-8?q?acto=20#4099=20review=20comments=20(#4102)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit in #4099, merging won a race with review. this applies suggestions from: * https://github.com/penumbra-zone/penumbra/pull/4099/files/6fbc35c26ced0ebf7b751aee076c342de94a5427#r1538264796 * https://github.com/penumbra-zone/penumbra/pull/4099/files/6fbc35c26ced0ebf7b751aee076c342de94a5427#r1538253529 --- .../component/validator_handler/uptime_tracker.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/core/component/stake/src/component/validator_handler/uptime_tracker.rs b/crates/core/component/stake/src/component/validator_handler/uptime_tracker.rs index 3c3190a98c..bd656db61f 100644 --- a/crates/core/component/stake/src/component/validator_handler/uptime_tracker.rs +++ b/crates/core/component/stake/src/component/validator_handler/uptime_tracker.rs @@ -27,8 +27,8 @@ use { /// A bundle of information about a validator used to track its uptime. type ValidatorInformation = (IdentityKey, tendermint::PublicKey, Uptime); -/// A collection of tasks retrieving [`ValidatorInformation`]. -type Lookups = JoinSet>>; +/// The output of a [`ValidatorUptimeTracker::spawn_validator_lookup_fut()`] task. +type LookupResult = anyhow::Result>; /// Tracks validator uptimes. /// @@ -71,7 +71,7 @@ pub trait ValidatorUptimeTracker: StateWrite { // Since we don't have a lookup from "addresses" to identity keys, // iterate over our app's validators, and match them up with the vote data. // We can fetch all the data required for processing each validator concurrently: - let mut lookups = Lookups::new(); + let mut lookups = JoinSet::new(); let mut validator_identity_stream = self.consensus_set_stream()?; while let Some(identity_key) = validator_identity_stream.next().await.transpose()? { self.spawn_validator_lookup_fut(identity_key, &mut lookups); @@ -82,7 +82,7 @@ pub trait ValidatorUptimeTracker: StateWrite { // independent, this doesn't introduce any nondeterminism into the complete state change. while let Some(data) = lookups.join_next().await.transpose()? { if let Some(validator_info) = data? { - self.process_vote(validator_info, &did_address_vote, ¶ms, height) + self.process_validator_uptime(validator_info, &did_address_vote, ¶ms, height) .await?; } } @@ -103,7 +103,7 @@ pub trait ValidatorUptimeTracker: StateWrite { fn spawn_validator_lookup_fut( &self, identity_key: crate::IdentityKey, - lookups: &mut Lookups, + lookups: &mut JoinSet, ) -> AbortHandle { // Define, but do not yet `.await` upon, a collection of futures fetching information // about a validator. @@ -148,7 +148,7 @@ pub trait ValidatorUptimeTracker: StateWrite { ) } - async fn process_vote( + async fn process_validator_uptime( &mut self, (identity_key, consensus_key, mut uptime): ValidatorInformation, did_address_vote: &BTreeMap, From 039fdb2ec0f882d2564bbd5e6469d869bf46bca1 Mon Sep 17 00:00:00 2001 From: Ava Howell Date: Wed, 20 Mar 2024 18:56:05 -0700 Subject: [PATCH 014/168] pcli: round withdrawal timestamp --- crates/bin/pcli/src/command/tx.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/bin/pcli/src/command/tx.rs b/crates/bin/pcli/src/command/tx.rs index 4395b2510a..5ad5f86e68 100644 --- a/crates/bin/pcli/src/command/tx.rs +++ b/crates/bin/pcli/src/command/tx.rs @@ -1051,16 +1051,22 @@ impl TxCmd { } }; - // get the current time on the local machine - let current_time_u64_ms = SystemTime::now() + // get the current time on the local machine (rounded to the nearest even second) + let mut current_time_s = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") - .as_nanos() as u64; + .as_secs() as u64; + + if current_time_s % 2 != 0 { + current_time_s += 1 + } + + let current_time_ns = current_time_s * 1_000_000_000; let mut timeout_timestamp = *timeout_timestamp; if timeout_timestamp == 0u64 { // add 2 days to current time - timeout_timestamp = current_time_u64_ms + 1.728e14 as u64; + timeout_timestamp = current_time_ns + 1.728e14 as u64; } fn parse_denom_and_amount(value_str: &str) -> anyhow::Result<(Amount, Metadata)> { From 6ec15ea48c2d516697168c73b9b38c54f710a69f Mon Sep 17 00:00:00 2001 From: Ava Howell Date: Wed, 20 Mar 2024 19:04:38 -0700 Subject: [PATCH 015/168] add chain rule: enforce nearest-even-second timestamps for withdrawals --- crates/bin/pcli/src/command/tx.rs | 15 ++++++--------- .../shielded-pool/src/ics20_withdrawal.rs | 11 ++++++++++- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/crates/bin/pcli/src/command/tx.rs b/crates/bin/pcli/src/command/tx.rs index 5ad5f86e68..65e81e8965 100644 --- a/crates/bin/pcli/src/command/tx.rs +++ b/crates/bin/pcli/src/command/tx.rs @@ -1051,17 +1051,11 @@ impl TxCmd { } }; - // get the current time on the local machine (rounded to the nearest even second) - let mut current_time_s = SystemTime::now() + // get the current time on the local machine + let current_time_ns = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") - .as_secs() as u64; - - if current_time_s % 2 != 0 { - current_time_s += 1 - } - - let current_time_ns = current_time_s * 1_000_000_000; + .as_nanos() as u64; let mut timeout_timestamp = *timeout_timestamp; if timeout_timestamp == 0u64 { @@ -1069,6 +1063,9 @@ impl TxCmd { timeout_timestamp = current_time_ns + 1.728e14 as u64; } + // round to the nearest even second + timeout_timestamp += timeout_timestamp % 2_000_000_000; + fn parse_denom_and_amount(value_str: &str) -> anyhow::Result<(Amount, Metadata)> { let denom_re = Regex::new(r"^([0-9.]+)(.+)$").context("denom regex invalid")?; if let Some(captures) = denom_re.captures(value_str) { diff --git a/crates/core/component/shielded-pool/src/ics20_withdrawal.rs b/crates/core/component/shielded-pool/src/ics20_withdrawal.rs index e548b471fb..76bf3aea16 100644 --- a/crates/core/component/shielded-pool/src/ics20_withdrawal.rs +++ b/crates/core/component/shielded-pool/src/ics20_withdrawal.rs @@ -33,7 +33,7 @@ pub struct Ics20Withdrawal { // prevent relayer censorship attacks. The core IBC implementation does this // in its handling of validation of timeouts. pub timeout_height: IbcHeight, - // the timestamp at which this transfer expires. + // the timestamp at which this transfer expires, in nanoseconds after unix epoch. pub timeout_time: u64, // the source channel used for the withdrawal pub source_channel: ChannelId, @@ -77,6 +77,15 @@ impl Ics20Withdrawal { anyhow::bail!("timeout time must be non-zero"); } + // in order to prevent clients from inadvertantly identifying themselves by their clock + // skew, enforce that timeout time is rounded to the nearest even second + if self.timeout_time % 2_000_000_000 != 0 { + anyhow::bail!( + "withdrawal timeout timestamp {} is not an even number of seconds", + self.timeout_time + ); + } + // NOTE: we could validate the destination chain address as bech32 to prevent mistyped // addresses, but this would preclude sending to chains that don't use bech32 addresses. From abe7730b91caa18598dd971fd45072f35b221ce0 Mon Sep 17 00:00:00 2001 From: Ava Howell Date: Thu, 21 Mar 2024 13:11:19 -0700 Subject: [PATCH 016/168] round to nearest single second --- crates/bin/pcli/src/command/tx.rs | 4 ++-- crates/core/component/shielded-pool/src/ics20_withdrawal.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bin/pcli/src/command/tx.rs b/crates/bin/pcli/src/command/tx.rs index 65e81e8965..18a4efb525 100644 --- a/crates/bin/pcli/src/command/tx.rs +++ b/crates/bin/pcli/src/command/tx.rs @@ -1063,8 +1063,8 @@ impl TxCmd { timeout_timestamp = current_time_ns + 1.728e14 as u64; } - // round to the nearest even second - timeout_timestamp += timeout_timestamp % 2_000_000_000; + // round to the nearest second + timeout_timestamp += timeout_timestamp % 1_000_000_000; fn parse_denom_and_amount(value_str: &str) -> anyhow::Result<(Amount, Metadata)> { let denom_re = Regex::new(r"^([0-9.]+)(.+)$").context("denom regex invalid")?; diff --git a/crates/core/component/shielded-pool/src/ics20_withdrawal.rs b/crates/core/component/shielded-pool/src/ics20_withdrawal.rs index 76bf3aea16..efff1418e6 100644 --- a/crates/core/component/shielded-pool/src/ics20_withdrawal.rs +++ b/crates/core/component/shielded-pool/src/ics20_withdrawal.rs @@ -78,10 +78,10 @@ impl Ics20Withdrawal { } // in order to prevent clients from inadvertantly identifying themselves by their clock - // skew, enforce that timeout time is rounded to the nearest even second - if self.timeout_time % 2_000_000_000 != 0 { + // skew, enforce that timeout time is rounded to the nearest second + if self.timeout_time % 1_000_000_000 != 0 { anyhow::bail!( - "withdrawal timeout timestamp {} is not an even number of seconds", + "withdrawal timeout timestamp {} is not rounded to one second", self.timeout_time ); } From 6637a8ab4aa844ebc7602f681f4014d131ce15eb Mon Sep 17 00:00:00 2001 From: Ava Howell Date: Thu, 21 Mar 2024 15:32:17 -0700 Subject: [PATCH 017/168] enforce timestamp resolution in ics20withdrawl to minutes, round to hours in pcli --- crates/bin/pcli/src/command/tx.rs | 4 ++-- crates/core/component/shielded-pool/src/ics20_withdrawal.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bin/pcli/src/command/tx.rs b/crates/bin/pcli/src/command/tx.rs index 18a4efb525..99dae32520 100644 --- a/crates/bin/pcli/src/command/tx.rs +++ b/crates/bin/pcli/src/command/tx.rs @@ -1063,8 +1063,8 @@ impl TxCmd { timeout_timestamp = current_time_ns + 1.728e14 as u64; } - // round to the nearest second - timeout_timestamp += timeout_timestamp % 1_000_000_000; + // round to the nearest hour + timeout_timestamp += timeout_timestamp % 3600_000_000_000; fn parse_denom_and_amount(value_str: &str) -> anyhow::Result<(Amount, Metadata)> { let denom_re = Regex::new(r"^([0-9.]+)(.+)$").context("denom regex invalid")?; diff --git a/crates/core/component/shielded-pool/src/ics20_withdrawal.rs b/crates/core/component/shielded-pool/src/ics20_withdrawal.rs index efff1418e6..94ad359491 100644 --- a/crates/core/component/shielded-pool/src/ics20_withdrawal.rs +++ b/crates/core/component/shielded-pool/src/ics20_withdrawal.rs @@ -78,8 +78,8 @@ impl Ics20Withdrawal { } // in order to prevent clients from inadvertantly identifying themselves by their clock - // skew, enforce that timeout time is rounded to the nearest second - if self.timeout_time % 1_000_000_000 != 0 { + // skew, enforce that timeout time is rounded to the nearest minute + if self.timeout_time % 60_000_000_000 != 0 { anyhow::bail!( "withdrawal timeout timestamp {} is not rounded to one second", self.timeout_time From 181b110ec833c3a2d54e15d91f3178b1a928b260 Mon Sep 17 00:00:00 2001 From: Ava Howell Date: Fri, 22 Mar 2024 00:57:48 -0700 Subject: [PATCH 018/168] change pcli default to round timestamps to 10 minutes --- crates/bin/pcli/src/command/tx.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bin/pcli/src/command/tx.rs b/crates/bin/pcli/src/command/tx.rs index 99dae32520..8e4d1356b2 100644 --- a/crates/bin/pcli/src/command/tx.rs +++ b/crates/bin/pcli/src/command/tx.rs @@ -1063,8 +1063,8 @@ impl TxCmd { timeout_timestamp = current_time_ns + 1.728e14 as u64; } - // round to the nearest hour - timeout_timestamp += timeout_timestamp % 3600_000_000_000; + // round to the nearest 10 minutes + timeout_timestamp += timeout_timestamp % 600_000_000_000; fn parse_denom_and_amount(value_str: &str) -> anyhow::Result<(Amount, Metadata)> { let denom_re = Regex::new(r"^([0-9.]+)(.+)$").context("denom regex invalid")?; From 5474c9ed6dab3236b9c525d1156b2ee6f7a8741e Mon Sep 17 00:00:00 2001 From: Ava Howell Date: Mon, 25 Mar 2024 13:34:17 -0700 Subject: [PATCH 019/168] fix error text for timeout timestamp --- crates/core/component/shielded-pool/src/ics20_withdrawal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/core/component/shielded-pool/src/ics20_withdrawal.rs b/crates/core/component/shielded-pool/src/ics20_withdrawal.rs index 94ad359491..60256c6a74 100644 --- a/crates/core/component/shielded-pool/src/ics20_withdrawal.rs +++ b/crates/core/component/shielded-pool/src/ics20_withdrawal.rs @@ -81,7 +81,7 @@ impl Ics20Withdrawal { // skew, enforce that timeout time is rounded to the nearest minute if self.timeout_time % 60_000_000_000 != 0 { anyhow::bail!( - "withdrawal timeout timestamp {} is not rounded to one second", + "withdrawal timeout timestamp {} is not rounded to one minute", self.timeout_time ); } From 85823bd19570b36319c56fbbe6940d8b0791df2c Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Tue, 26 Mar 2024 11:42:56 -0400 Subject: [PATCH 020/168] =?UTF-8?q?tests(app):=20=F0=9F=8C=AF=20reorganize?= =?UTF-8?q?,=20rename=20mock=20consensus=20integration=20tests=20(#4103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > it's probably about time to rename the app/tests/mock_consensus\w*.rs tests, and the test cases contained therein. at this point they're becoming the app test suite, so phrasing them as though they're exercising the mock engine is a little backwards this follows up on this note, from the discord's #testing channel. --- ...can_define_and_delegate_to_a_validator.rs} | 6 +- ...app_can_spend_notes_and_detect_outputs.rs} | 84 +----------------- crates/core/app/tests/init_chain.rs | 85 +++++++++++++++++++ 3 files changed, 89 insertions(+), 86 deletions(-) rename crates/core/app/tests/{mock_consensus_staking.rs => app_can_define_and_delegate_to_a_validator.rs} (99%) rename crates/core/app/tests/{mock_consensus.rs => app_can_spend_notes_and_detect_outputs.rs} (54%) create mode 100644 crates/core/app/tests/init_chain.rs diff --git a/crates/core/app/tests/mock_consensus_staking.rs b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs similarity index 99% rename from crates/core/app/tests/mock_consensus_staking.rs rename to crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs index 3aa6cda6cb..606bd684d5 100644 --- a/crates/core/app/tests/mock_consensus_staking.rs +++ b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs @@ -26,7 +26,7 @@ use { const EPOCH_DURATION: u64 = 8; #[tokio::test] -async fn mock_consensus_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { +async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { // Install a test logger, acquire some temporary storage, and start the test node. let guard = common::set_tracing_subscriber(); let storage = TempStorage::new().await?; @@ -121,11 +121,9 @@ async fn mock_consensus_can_define_and_delegate_to_a_validator() -> anyhow::Resu let new_validator_consensus = new_validator_consensus_sk.verification_key(); // Insert the validator's consensus keypair into the keyring so it can be used to sign blocks. - /* node.keyring_mut() // Keyring should just be a BTreeMap rather than creating a new API - .insert(validator_consensus.clone(), validator_consensus_sk); - */ + .insert(new_validator_consensus, new_validator_consensus_sk); // Now define the validator's configuration data. let new_validator = Validator { diff --git a/crates/core/app/tests/mock_consensus.rs b/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs similarity index 54% rename from crates/core/app/tests/mock_consensus.rs rename to crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs index f5cd9e5a1d..580145a74f 100644 --- a/crates/core/app/tests/mock_consensus.rs +++ b/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs @@ -1,8 +1,3 @@ -//! App integration tests using mock consensus. -// -// Note: these should eventually replace the existing test cases. mock consensus tests are placed -// here while the engine is still in development. See #3588. - mod common; use { @@ -12,9 +7,8 @@ use { penumbra_mock_client::MockClient, penumbra_mock_consensus::TestNode, penumbra_proto::DomainType, - penumbra_sct::component::{clock::EpochRead, tree::SctRead as _}, + penumbra_sct::component::tree::SctRead as _, penumbra_shielded_pool::{OutputPlan, SpendPlan}, - penumbra_stake::component::validator_handler::ValidatorDataRead as _, penumbra_transaction::{ memo::MemoPlaintext, plan::MemoPlan, TransactionParameters, TransactionPlan, }, @@ -23,82 +17,8 @@ use { tracing::info, }; -/// Exercises that a test node can be instantiated using the consensus service. -#[tokio::test] -async fn mock_consensus_can_send_an_init_chain_request() -> anyhow::Result<()> { - // Install a test logger, acquire some temporary storage, and start the test node. - let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; - let _ = common::start_test_node(&storage).await?; - - // Free our temporary storage. - drop(storage); - drop(guard); - - Ok(()) -} - -/// Exercises that the mock consensus engine can provide a single genesis validator. -#[tokio::test] -async fn mock_consensus_can_define_a_genesis_validator() -> anyhow::Result<()> { - // Install a test logger, acquire some temporary storage, and start the test node. - let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; - let _test_node = common::start_test_node(&storage).await?; - - let snapshot = storage.latest_snapshot(); - let validators = snapshot - .validator_definitions() - .tap(|_| info!("getting validator definitions")) - .await?; - match validators.as_slice() { - [v] => { - let identity_key = v.identity_key; - let status = snapshot - .get_validator_state(&identity_key) - .await? - .ok_or_else(|| anyhow!("could not find validator status"))?; - assert_eq!( - status, - penumbra_stake::validator::State::Active, - "validator should be active" - ); - } - unexpected => panic!("there should be one validator, got: {unexpected:?}"), - } - - // Free our temporary storage. - drop(storage); - drop(guard); - - Ok(()) -} - -/// Exercises that a series of empty blocks, with no validator set present, can be successfully -/// executed by the consensus service. -#[tokio::test] -async fn mock_consensus_can_send_a_sequence_of_empty_blocks() -> anyhow::Result<()> { - // Install a test logger, acquire some temporary storage, and start the test node. - let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; - let mut test_node = common::start_test_node(&storage).await?; - - let height = || async { storage.latest_snapshot().get_block_height().await }; - - // Fast forward eight blocks, and show that the height is 8 after doing so. - assert_eq!(height().await?, 0, "height should begin at 0"); - test_node.fast_forward(8).await?; - assert_eq!(height().await?, 8_u64, "height should grow"); - - // Free our temporary storage. - drop(storage); - drop(guard); - - Ok(()) -} - #[tokio::test] -async fn mock_consensus_can_spend_notes_and_detect_outputs() -> anyhow::Result<()> { +async fn app_can_spend_notes_and_detect_outputs() -> anyhow::Result<()> { // Install a test logger, acquire some temporary storage, and start the test node. let guard = common::set_tracing_subscriber(); let storage = TempStorage::new().await?; diff --git a/crates/core/app/tests/init_chain.rs b/crates/core/app/tests/init_chain.rs new file mode 100644 index 0000000000..b983f3c420 --- /dev/null +++ b/crates/core/app/tests/init_chain.rs @@ -0,0 +1,85 @@ +//! App integration tests using mock consensus. +// +// Note: these should eventually replace the existing test cases. mock consensus tests are placed +// here while the engine is still in development. See #3588. + +mod common; + +use { + anyhow::anyhow, cnidarium::TempStorage, penumbra_sct::component::clock::EpochRead, + penumbra_stake::component::validator_handler::ValidatorDataRead as _, tap::Tap, tracing::info, +}; + +/// Exercises that a test node can be instantiated using the consensus service. +#[tokio::test] +async fn mock_consensus_can_send_an_init_chain_request() -> anyhow::Result<()> { + // Install a test logger, acquire some temporary storage, and start the test node. + let guard = common::set_tracing_subscriber(); + let storage = TempStorage::new().await?; + let _ = common::start_test_node(&storage).await?; + + // Free our temporary storage. + drop(storage); + drop(guard); + + Ok(()) +} + +/// Exercises that the mock consensus engine can provide a single genesis validator. +#[tokio::test] +async fn mock_consensus_can_define_a_genesis_validator() -> anyhow::Result<()> { + // Install a test logger, acquire some temporary storage, and start the test node. + let guard = common::set_tracing_subscriber(); + let storage = TempStorage::new().await?; + let _test_node = common::start_test_node(&storage).await?; + + let snapshot = storage.latest_snapshot(); + let validators = snapshot + .validator_definitions() + .tap(|_| info!("getting validator definitions")) + .await?; + match validators.as_slice() { + [v] => { + let identity_key = v.identity_key; + let status = snapshot + .get_validator_state(&identity_key) + .await? + .ok_or_else(|| anyhow!("could not find validator status"))?; + assert_eq!( + status, + penumbra_stake::validator::State::Active, + "validator should be active" + ); + } + unexpected => panic!("there should be one validator, got: {unexpected:?}"), + } + + // Free our temporary storage. + drop(storage); + drop(guard); + + Ok(()) +} + +/// Exercises that a series of empty blocks, with no validator set present, can be successfully +/// executed by the consensus service. +#[tokio::test] +async fn mock_consensus_can_send_a_sequence_of_empty_blocks() -> anyhow::Result<()> { + // Install a test logger, acquire some temporary storage, and start the test node. + let guard = common::set_tracing_subscriber(); + let storage = TempStorage::new().await?; + let mut test_node = common::start_test_node(&storage).await?; + + let height = || async { storage.latest_snapshot().get_block_height().await }; + + // Fast forward eight blocks, and show that the height is 8 after doing so. + assert_eq!(height().await?, 0, "height should begin at 0"); + test_node.fast_forward(8).await?; + assert_eq!(height().await?, 8_u64, "height should grow"); + + // Free our temporary storage. + drop(storage); + drop(guard); + + Ok(()) +} From 1475239aa4461a4d667542f979685be641cd3c1e Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Tue, 26 Mar 2024 12:31:09 -0400 Subject: [PATCH 021/168] app: check asset id in fee stateful check (#4104) Fix #4106. @hdevalence I remember discussing with you making the `fee` method return a `Value`, but I don't recall why we decided against it. --- .../action_handler/transaction/stateful.rs | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/crates/core/app/src/action_handler/transaction/stateful.rs b/crates/core/app/src/action_handler/transaction/stateful.rs index 424cfcf1db..7241d1a6a6 100644 --- a/crates/core/app/src/action_handler/transaction/stateful.rs +++ b/crates/core/app/src/action_handler/transaction/stateful.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{ensure, Result}; use cnidarium::StateRead; use penumbra_fee::component::StateReadExt as _; use penumbra_sct::component::clock::EpochRead; @@ -89,18 +89,23 @@ pub async fn fee_greater_than_base_fee( .expect("gas prices must be present in state"); let transaction_base_price = current_gas_prices.fee(&transaction.gas_cost()); + let user_supplied_fee = transaction.transaction_body().transaction_parameters.fee; + let user_supplied_fee_amount = user_supplied_fee.amount(); + let user_supplied_fee_asset_id = user_supplied_fee.asset_id(); - if transaction - .transaction_body() - .transaction_parameters - .fee - .amount() - >= transaction_base_price - { - Ok(()) - } else { - Err(anyhow::anyhow!( - "consensus rule violated: paid transaction fee must be greater than or equal to transaction's base fee" - )) - } + ensure!( + user_supplied_fee_amount >= transaction_base_price, + "fee must be greater than or equal to the transaction base price (supplied: {}, base: {})", + user_supplied_fee_amount, + transaction_base_price + ); + + // We split the check to provide granular error messages. + ensure!( + user_supplied_fee_asset_id == *penumbra_asset::STAKING_TOKEN_ASSET_ID, + "fee must be paid in staking tokens (found: {})", + user_supplied_fee_asset_id + ); + + Ok(()) } From 589967375e94a3bc495e854178e3eae5ec6adb7c Mon Sep 17 00:00:00 2001 From: Henry de Valence Date: Mon, 25 Mar 2024 00:01:51 -0700 Subject: [PATCH 022/168] dex: move read methods out of PositionManager --- .../dex/src/component/position_manager.rs | 94 +++++++++---------- .../dex/src/component/router/path_search.rs | 2 +- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/crates/core/component/dex/src/component/position_manager.rs b/crates/core/component/dex/src/component/position_manager.rs index 9854418fd6..83daf34d38 100644 --- a/crates/core/component/dex/src/component/position_manager.rs +++ b/crates/core/component/dex/src/component/position_manager.rs @@ -83,6 +83,53 @@ pub trait PositionRead: StateRead { self.object_get(state_key::pending_position_closures()) .unwrap_or_default() } + + /// Returns the list of candidate assets to route through for a trade from `from`. + /// Combines a list of fixed candidates with a list of liquidity-based candidates. + /// This ensures that the fixed candidates are always considered, minimizing + /// the risk of attacks on routing. + fn candidate_set( + &self, + from: asset::Id, + fixed_candidates: Arc>, + ) -> Pin> + Send>> { + // Clone the fixed candidates Arc so it can be moved into the stream filter's future. + let fc = fixed_candidates.clone(); + let mut dynamic_candidates = self + .ordered_routable_assets(&from) + .filter(move |c| { + future::ready(!fc.contains(c.as_ref().expect("failed to fetch candidate"))) + }) + .take(DYNAMIC_ASSET_LIMIT); + try_stream! { + // First stream the fixed candidates, so those can be processed while the dynamic candidates are fetched. + for candidate in fixed_candidates.iter() { + yield candidate.clone(); + } + + // Yield the liquidity-based candidates. Note that this _may_ include some assets already included in the fixed set. + while let Some(candidate) = dynamic_candidates + .next().await { + yield candidate.expect("failed to fetch candidate"); + } + } + .boxed() + } + + /// Returns a stream of [`asset::Id`] routable from a given asset, ordered by liquidity. + fn ordered_routable_assets( + &self, + from: &asset::Id, + ) -> Pin> + Send + 'static>> { + let prefix = state_key::internal::routable_assets::prefix(from); + tracing::trace!(prefix = ?EscapedByteSlice(&prefix), "searching for routable assets by liquidity"); + self.nonverifiable_prefix_raw(&prefix) + .map(|entry| match entry { + Ok((_, v)) => Ok(asset::Id::decode(&*v)?), + Err(e) => Err(e), + }) + .boxed() + } } impl PositionRead for T {} @@ -195,53 +242,6 @@ pub trait PositionManager: StateWrite + PositionRead { _ => position, } } - - /// Returns the list of candidate assets to route through for a trade from `from`. - /// Combines a list of fixed candidates with a list of liquidity-based candidates. - /// This ensures that the fixed candidates are always considered, minimizing - /// the risk of attacks on routing. - fn candidate_set( - &self, - from: asset::Id, - fixed_candidates: Arc>, - ) -> Pin> + Send>> { - // Clone the fixed candidates Arc so it can be moved into the stream filter's future. - let fc = fixed_candidates.clone(); - let mut dynamic_candidates = self - .ordered_routable_assets(&from) - .filter(move |c| { - future::ready(!fc.contains(c.as_ref().expect("failed to fetch candidate"))) - }) - .take(DYNAMIC_ASSET_LIMIT); - try_stream! { - // First stream the fixed candidates, so those can be processed while the dynamic candidates are fetched. - for candidate in fixed_candidates.iter() { - yield candidate.clone(); - } - - // Yield the liquidity-based candidates. Note that this _may_ include some assets already included in the fixed set. - while let Some(candidate) = dynamic_candidates - .next().await { - yield candidate.expect("failed to fetch candidate"); - } - } - .boxed() - } - - /// Returns a stream of [`asset::Id`] routable from a given asset, ordered by liquidity. - fn ordered_routable_assets( - &self, - from: &asset::Id, - ) -> Pin> + Send + 'static>> { - let prefix = state_key::internal::routable_assets::prefix(from); - tracing::trace!(prefix = ?EscapedByteSlice(&prefix), "searching for routable assets by liquidity"); - self.nonverifiable_prefix_raw(&prefix) - .map(|entry| match entry { - Ok((_, v)) => Ok(asset::Id::decode(&*v)?), - Err(e) => Err(e), - }) - .boxed() - } } impl PositionManager for T {} diff --git a/crates/core/component/dex/src/component/router/path_search.rs b/crates/core/component/dex/src/component/router/path_search.rs index b98567491e..a111e32294 100644 --- a/crates/core/component/dex/src/component/router/path_search.rs +++ b/crates/core/component/dex/src/component/router/path_search.rs @@ -9,7 +9,7 @@ use penumbra_num::fixpoint::U128x128; use tokio::task::JoinSet; use tracing::{instrument, Instrument}; -use crate::component::PositionManager; +use crate::component::PositionRead as _; use super::{Path, PathCache, PathEntry, RoutingParams, SharedPathCache}; From 7abae37d44f7cc915358d1c7ef5a58b236a82c48 Mon Sep 17 00:00:00 2001 From: Henry de Valence Date: Mon, 25 Mar 2024 00:05:37 -0700 Subject: [PATCH 023/168] dex: remove old limit order handling --- .../src/component/circuit_breaker/value.rs | 4 +- .../dex/src/component/position_manager.rs | 40 ------------------- 2 files changed, 2 insertions(+), 42 deletions(-) diff --git a/crates/core/component/dex/src/component/circuit_breaker/value.rs b/crates/core/component/dex/src/component/circuit_breaker/value.rs index eb650207a7..75d1b1a649 100644 --- a/crates/core/component/dex/src/component/circuit_breaker/value.rs +++ b/crates/core/component/dex/src/component/circuit_breaker/value.rs @@ -52,7 +52,7 @@ mod tests { use crate::component::{StateReadExt as _, StateWriteExt as _}; use crate::lp::plan::PositionWithdrawPlan; use crate::{ - component::{router::limit_buy, tests::TempStorageExt, PositionManager as _}, + component::{router::limit_buy, tests::TempStorageExt}, state_key, DirectedUnitPair, }; use crate::{BatchSwapOutputData, PositionOpen}; @@ -224,7 +224,7 @@ mod tests { let id = buy_1.id(); - let position = state_tx.handle_limit_order(&None, buy_1); + let position = buy_1; state_tx.index_position_by_price(&position); state_tx .update_available_liquidity(&position, &None) diff --git a/crates/core/component/dex/src/component/position_manager.rs b/crates/core/component/dex/src/component/position_manager.rs index 83daf34d38..e4988949c4 100644 --- a/crates/core/component/dex/src/component/position_manager.rs +++ b/crates/core/component/dex/src/component/position_manager.rs @@ -191,11 +191,6 @@ pub trait PositionManager: StateWrite + PositionRead { // reserves or the position state might have invalidated them. self.deindex_position_by_price(&position); - // currently, we are disabling limit orders due to the complexity involved in managing - // limit orders in the dex state machine (see - // https://github.com/penumbra-zone/penumbra/issues/3850#issuecomment-1977300433) - // let position = self.handle_limit_order(&prev, position); - // Only index the position's liquidity if it is active. if position.state == position::State::Opened { self.index_position_by_price(&position); @@ -207,41 +202,6 @@ pub trait PositionManager: StateWrite + PositionRead { self.put(state_key::position_by_id(&id), position); Ok(()) } - - /// Handle a limit order, inspecting it previous state to determine if it - /// has been filled, and if so, marking it as closed. If the position is - /// not a limit order, or has not been filled, it is returned unchanged. - fn handle_limit_order( - &self, - prev_position: &Option, - position: Position, - ) -> Position { - let id = position.id(); - match prev_position { - Some(_) if position.close_on_fill => { - // It's technically possible for a limit order to be partially filled, - // and unfilled on the other side. In this case, we would close it prematurely. - // However, because of the arbitrage dynamics we expect that in practice an order - // gets completely filled or not at all. - if position.reserves.r1 == Amount::zero() || position.reserves.r2 == Amount::zero() - { - tracing::debug!(?id, "limit order filled, setting state to closed"); - Position { - state: position::State::Closed, - ..position - } - } else { - tracing::debug!(?id, "limit order partially filled, keeping open"); - position - } - } - None if position.close_on_fill => { - tracing::debug!(?id, "detected a newly opened limit order"); - position - } - _ => position, - } - } } impl PositionManager for T {} From b3662ee1ed7adea8f77cacbcd653cca24c377964 Mon Sep 17 00:00:00 2001 From: Henry de Valence Date: Mon, 25 Mar 2024 11:50:02 -0700 Subject: [PATCH 024/168] dex: internalize `PositionManager::put_position` This implements an API suggestion originally made by @cronokirby to expose context-specific methods for updating positions. We maintain a single `put_position` method on the `Inner` trait so that we're able to have a single point where we edit any necessary indexes, but this is now not exposed to callers. Instead they call a scope-specific method that calls `put_position` internally. Currently there's still room to improve the API safety but we'll want to potentially change the signature of `put_position` to ensure that this remains efficient. It would be ideal if it were possible to do all reads up front and concurrently. Further investigation on this point would be desirable. This commit has a security-critical change in that the logic for position withdrawals' balance commitment changes, and it'd be good to get review on that part in particular. --- .../component/action_handler/position/open.rs | 16 +- .../action_handler/position/withdraw.rs | 78 +----- .../dex/src/component/position_manager.rs | 127 +++++++++- .../dex/src/component/router/fill_route.rs | 6 +- .../dex/src/component/router/tests.rs | 229 ++++++++---------- .../core/component/dex/src/component/tests.rs | 80 ++---- crates/core/component/dex/src/event.rs | 20 +- 7 files changed, 259 insertions(+), 297 deletions(-) diff --git a/crates/core/component/dex/src/component/action_handler/position/open.rs b/crates/core/component/dex/src/component/action_handler/position/open.rs index 588c8972b7..cdc9dd2995 100644 --- a/crates/core/component/dex/src/component/action_handler/position/open.rs +++ b/crates/core/component/dex/src/component/action_handler/position/open.rs @@ -2,11 +2,9 @@ use anyhow::Result; use async_trait::async_trait; use cnidarium::StateWrite; use cnidarium_component::ActionHandler; -use penumbra_proto::StateWriteProto as _; use crate::{ - component::{PositionManager, PositionRead, ValueCircuitBreaker}, - event, + component::PositionManager, lp::{action::PositionOpen, position}, }; @@ -31,17 +29,7 @@ impl ActionHandler for PositionOpen { } async fn check_and_execute(&self, mut state: S) -> Result<()> { - // Validate that the position ID doesn't collide - state.check_position_id_unused(&self.position.id()).await?; - - // Credit the DEX for the inflows from this position. - // TODO: in a future PR, split current PositionManager to PositionManagerInner - // and fold this into a position open method - state.vcb_credit(self.position.reserves_1()).await?; - state.vcb_credit(self.position.reserves_2()).await?; - - state.put_position(self.position.clone()).await?; - state.record_proto(event::position_open(self)); + state.open_position(self.position.clone()).await?; Ok(()) } } diff --git a/crates/core/component/dex/src/component/action_handler/position/withdraw.rs b/crates/core/component/dex/src/component/action_handler/position/withdraw.rs index a012ae4c9c..b1487fbd4f 100644 --- a/crates/core/component/dex/src/component/action_handler/position/withdraw.rs +++ b/crates/core/component/dex/src/component/action_handler/position/withdraw.rs @@ -1,16 +1,11 @@ -use anyhow::{anyhow, Result}; +use anyhow::Result; use ark_ff::Zero; use async_trait::async_trait; use cnidarium::StateWrite; use cnidarium_component::ActionHandler; use decaf377::Fr; -use penumbra_proto::StateWriteProto; -use crate::{ - component::{PositionManager, PositionRead, ValueCircuitBreaker}, - event, - lp::{action::PositionWithdraw, position, Reserves}, -}; +use crate::{component::PositionManager, lp::action::PositionWithdraw}; #[async_trait] /// Debits a closed position NFT and credits a withdrawn position NFT and the final reserves. @@ -27,25 +22,22 @@ impl ActionHandler for PositionWithdraw { // we need to ensure that we're checking the reserves at the moment we execute // the withdrawal, to prevent any possibility of TOCTOU attacks. - let mut metadata = state - .position_by_id(&self.position_id) - .await? - .ok_or_else(|| anyhow!("withdrew from unknown position {}", self.position_id))?; + // Execute the withdrawal, extracting the reserves from the position. + let actual_reserves = state + .withdraw_position(self.position_id, self.sequence) + .await?; - // First, check that the commitment to the amount the user is + // Next, and CRITICALLY, check that the commitment to the amount the user is // withdrawing is correct. // // Unlike other actions, where a balance commitment is used for // shielding a value, this commitment is used for compression, giving a // single commitment rather than a list of token amounts. - + // // Note: since this is forming a commitment only to the reserves, // we are implicitly setting the reward amount to 0. However, we can // add support for rewards in the future without client changes. - let expected_reserves_commitment = metadata - .reserves - .balance(&metadata.phi.pair) - .commit(Fr::zero()); + let expected_reserves_commitment = actual_reserves.commit(Fr::zero()); if self.reserves_commitment != expected_reserves_commitment { anyhow::bail!( @@ -55,58 +47,6 @@ impl ActionHandler for PositionWithdraw { ); } - // Next, check that the withdrawal is consistent with the position state. - // This should be redundant with the value balance mechanism (clients should - // only be able to get the required input LPNFTs if the state transitions are - // consistent), but we check it here for defense in depth. - if self.sequence == 0 { - if metadata.state != position::State::Closed { - anyhow::bail!( - "attempted to withdraw position {} with state {}, expected Closed", - self.position_id, - metadata.state - ); - } - } else { - if let position::State::Withdrawn { sequence } = metadata.state { - if sequence + 1 != self.sequence { - anyhow::bail!( - "attempted to withdraw position {} with sequence {}, expected {}", - self.position_id, - self.sequence, - sequence + 1 - ); - } - } else { - anyhow::bail!( - "attempted to withdraw position {} with state {}, expected Withdrawn", - self.position_id, - metadata.state - ); - } - } - - // Record an event prior to updating the position state, so we have access to - // the current reserves. - state.record_proto(event::position_withdraw(self, &metadata)); - - // Debit the DEX for the outflows from this position. - // TODO: in a future PR, split current PositionManager to PositionManagerInner - // and fold this into a position open method - state.vcb_debit(metadata.reserves_1()).await?; - state.vcb_debit(metadata.reserves_2()).await?; - - // Finally, update the position. This has two steps: - // - update the state with the correct sequence number; - // - zero out the reserves, to prevent double-withdrawals. - metadata.state = position::State::Withdrawn { - // We just checked that the supplied sequence number is incremented by 1 from prev. - sequence: self.sequence, - }; - metadata.reserves = Reserves::zero(); - - state.put_position(metadata).await?; - Ok(()) } } diff --git a/crates/core/component/dex/src/component/position_manager.rs b/crates/core/component/dex/src/component/position_manager.rs index e4988949c4..d6d8d268c8 100644 --- a/crates/core/component/dex/src/component/position_manager.rs +++ b/crates/core/component/dex/src/component/position_manager.rs @@ -7,13 +7,16 @@ use async_trait::async_trait; use cnidarium::{EscapedByteSlice, StateRead, StateWrite}; use futures::Stream; use futures::StreamExt; -use penumbra_asset::asset; +use penumbra_asset::{asset, Balance}; use penumbra_num::Amount; use penumbra_proto::DomainType; use penumbra_proto::{StateReadProto, StateWriteProto}; +use crate::event; use crate::lp::position::State; +use crate::lp::Reserves; use crate::{ + component::ValueCircuitBreaker, lp::position::{self, Position}, state_key, DirectedTradingPair, }; @@ -175,7 +178,124 @@ pub trait PositionManager: StateWrite + PositionRead { self.object_delete(state_key::pending_position_closures()); } + /// Opens a new position, updating all necessary indexes and checking for + /// its nonexistence prior to being opened. + #[tracing::instrument(level = "debug", skip_all)] + async fn open_position(&mut self, position: position::Position) -> Result<()> { + // Double-check that the position is in the `Opened` state + if position.state != position::State::Opened { + anyhow::bail!("attempted to open a position with a state besides `Opened`"); + } + + // Validate that the position ID doesn't collide + self.check_position_id_unused(&position.id()).await?; + + // Credit the DEX for the inflows from this position. + self.vcb_credit(position.reserves_1()).await?; + self.vcb_credit(position.reserves_2()).await?; + + // Finally, record the new position state. + self.record_proto(event::position_open(&position)); + self.put_position(position).await?; + + Ok(()) + } + + /// Record execution against an opened position. + #[tracing::instrument(level = "debug", skip_all)] + async fn position_execution(&mut self, post_execution_state: position::Position) -> Result<()> { + self.record_proto(event::position_execution(&post_execution_state)); + self.put_position(post_execution_state).await?; + Ok(()) + } + + /// Withdraw from a closed position, incrementing its sequence number. + /// + /// Updates the position's reserves and rewards to zero and returns the withdrawn balance. + #[tracing::instrument(level = "debug", skip(self))] + async fn withdraw_position( + &mut self, + position_id: position::Id, + sequence: u64, + ) -> Result { + let mut metadata = self + .position_by_id(&position_id) + .await? + .ok_or_else(|| anyhow::anyhow!("withdrew from unknown position {}", position_id))?; + + // Next, check that the withdrawal is consistent with the position state. + // This should be redundant with the value balance mechanism (clients should + // only be able to get the required input LPNFTs if the state transitions are + // consistent), but we check it here for defense in depth. + // + // This is just a check that sequence == current_sequence + 1, with extra logic + // so that we treat "closed" as "sequence -1". + if sequence == 0 { + if metadata.state != position::State::Closed { + anyhow::bail!( + "attempted to withdraw position {} with state {}, expected Closed", + position_id, + metadata.state + ); + } + } else { + if let position::State::Withdrawn { + sequence: current_sequence, + } = metadata.state + { + if current_sequence + 1 != sequence { + anyhow::bail!( + "attempted to withdraw position {} with sequence {}, expected {}", + position_id, + sequence, + current_sequence + 1 + ); + } + } else { + anyhow::bail!( + "attempted to withdraw position {} with state {}, expected Withdrawn", + position_id, + metadata.state + ); + } + } + + // Record an event prior to updating the position state, so we have access to + // the current reserves. + self.record_proto(event::position_withdraw(position_id, &metadata)); + + // Grab a copy of the final reserves of the position to return to the caller. + let reserves = metadata.reserves.balance(&metadata.phi.pair); + + // Debit the DEX for the outflows from this position. + // TODO: in a future PR, split current PositionManager to PositionManagerInner + // and fold this into a position open method + self.vcb_debit(metadata.reserves_1()).await?; + self.vcb_debit(metadata.reserves_2()).await?; + + // Finally, update the position. This has two steps: + // - update the state with the correct sequence number; + // - zero out the reserves, to prevent double-withdrawals. + metadata.state = position::State::Withdrawn { + // We just checked that the supplied sequence number is incremented by 1 from prev. + sequence, + }; + metadata.reserves = Reserves::zero(); + + self.put_position(metadata).await?; + + Ok(reserves) + } +} + +impl PositionManager for T {} + +#[async_trait] +pub(crate) trait Inner: StateWrite { /// Writes a position to the state, updating all necessary indexes. + /// + /// This should be the SOLE ENTRYPOINT for writing positions to the state. + /// All other position changes exposed by the `PositionManager` should run through here. #[tracing::instrument(level = "debug", skip(self, position), fields(id = ?position.id()))] async fn put_position(&mut self, position: position::Position) -> Result<()> { let id = position.id(); @@ -202,12 +322,7 @@ pub trait PositionManager: StateWrite + PositionRead { self.put(state_key::position_by_id(&id), position); Ok(()) } -} -impl PositionManager for T {} - -#[async_trait] -pub(crate) trait Inner: StateWrite { fn index_position_by_price(&mut self, position: &position::Position) { let (pair, phi) = (position.phi.pair, &position.phi); let id = position.id(); diff --git a/crates/core/component/dex/src/component/router/fill_route.rs b/crates/core/component/dex/src/component/router/fill_route.rs index 6f815f41ea..1f8ea5e1a1 100644 --- a/crates/core/component/dex/src/component/router/fill_route.rs +++ b/crates/core/component/dex/src/component/router/fill_route.rs @@ -412,11 +412,11 @@ impl Frontier { async fn save(&mut self) -> Result<()> { for position in &self.positions { - self.state.put_position(position.clone()).await?; + self.state.position_execution(position.clone()).await?; // Create an ABCI event signaling that the position was executed against self.state - .record_proto(event::position_execution(position.clone())); + .record_proto(event::position_execution(&position)); } Ok(()) } @@ -492,7 +492,7 @@ impl Frontier { // frontier. The other positions will be written out either when // they're fully consumed, or when we finish filling. self.state - .put_position(self.positions[index].clone()) + .position_execution(self.positions[index].clone()) .await .expect("writing to storage should not fail"); diff --git a/crates/core/component/dex/src/component/router/tests.rs b/crates/core/component/dex/src/component/router/tests.rs index 81ca29dc37..6618d902ec 100644 --- a/crates/core/component/dex/src/component/router/tests.rs +++ b/crates/core/component/dex/src/component/router/tests.rs @@ -8,7 +8,6 @@ use penumbra_num::{fixpoint::U128x128, Amount}; use rand_core::OsRng; use std::sync::Arc; -use crate::component::ValueCircuitBreaker; use crate::lp::SellOrder; use crate::DexParameters; use crate::{ @@ -326,16 +325,16 @@ async fn create_test_positions_basic(s: &mut S, misprice: bool) { }, ); - s.put_position(position_1).await.unwrap(); - s.put_position(position_2).await.unwrap(); - s.put_position(position_3).await.unwrap(); - s.put_position(position_4).await.unwrap(); - s.put_position(position_5).await.unwrap(); + s.open_position(position_1).await.unwrap(); + s.open_position(position_2).await.unwrap(); + s.open_position(position_3).await.unwrap(); + s.open_position(position_4).await.unwrap(); + s.open_position(position_5).await.unwrap(); if misprice { - s.put_position(position_6).await.unwrap(); + s.open_position(position_6).await.unwrap(); } - s.put_position(position_7).await.unwrap(); - s.put_position(position_8).await.unwrap(); + s.open_position(position_7).await.unwrap(); + s.open_position(position_8).await.unwrap(); } /// Create a `Position` to buy `asset_1` using `asset_2` with explicit p/q. @@ -436,8 +435,8 @@ async fn position_get_best_price() -> anyhow::Result<()> { r2: 0u64.into(), }, ); - state_tx.put_position(position_1.clone()).await.unwrap(); - state_tx.put_position(position_2.clone()).await.unwrap(); + state_tx.open_position(position_1.clone()).await.unwrap(); + state_tx.open_position(position_2.clone()).await.unwrap(); let positions = state_tx .positions_by_price(&pair) @@ -494,8 +493,8 @@ async fn test_multiple_similar_position() -> anyhow::Result<()> { let mut buy_2 = limit_buy(pair_1.clone(), 1u64.into(), price1); buy_1.nonce = [1u8; 32]; buy_2.nonce = [2u8; 32]; - state_tx.put_position(buy_1.clone()).await.unwrap(); - state_tx.put_position(buy_2.clone()).await.unwrap(); + state_tx.open_position(buy_1.clone()).await.unwrap(); + state_tx.open_position(buy_2.clone()).await.unwrap(); let mut p_1 = state_tx .best_position(&pair_1.into_directed_trading_pair()) @@ -504,7 +503,7 @@ async fn test_multiple_similar_position() -> anyhow::Result<()> { .expect("we just posted two positions"); assert_eq!(p_1.nonce, buy_1.nonce); p_1.reserves = p_1.reserves.flip(); - state_tx.put_position(p_1).await.unwrap(); + state_tx.position_execution(p_1).await.unwrap(); let mut p_2 = state_tx .best_position(&pair_1.into_directed_trading_pair()) @@ -513,7 +512,7 @@ async fn test_multiple_similar_position() -> anyhow::Result<()> { .expect("there is one position remaining"); assert_eq!(p_2.nonce, buy_2.nonce); p_2.reserves = p_2.reserves.flip(); - state_tx.put_position(p_2).await.unwrap(); + state_tx.position_execution(p_2).await.unwrap(); assert!(state_tx .best_position(&pair_1.into_directed_trading_pair()) @@ -568,8 +567,8 @@ async fn fill_route_constraint_stacked() -> anyhow::Result<()> { let buy_1 = limit_buy(pair_1.clone(), 3u64.into(), price2); let buy_2 = limit_buy(pair_1.clone(), 1u64.into(), price1); - state_tx.put_position(buy_1).await.unwrap(); - state_tx.put_position(buy_2).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); + state_tx.open_position(buy_2).await.unwrap(); /* pair 2 */ let price2 = Amount::from(2u64); @@ -579,10 +578,10 @@ async fn fill_route_constraint_stacked() -> anyhow::Result<()> { let buy_3 = limit_buy(pair_2.clone(), 50u64.into(), price1); let buy_4 = limit_buy(pair_2.clone(), 50u64.into(), price1); - state_tx.put_position(buy_1).await.unwrap(); - state_tx.put_position(buy_2).await.unwrap(); - state_tx.put_position(buy_3).await.unwrap(); - state_tx.put_position(buy_4).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); + state_tx.open_position(buy_2).await.unwrap(); + state_tx.open_position(buy_3).await.unwrap(); + state_tx.open_position(buy_4).await.unwrap(); /* pair 3 */ let price2000 = 2000u64.into(); @@ -597,11 +596,11 @@ async fn fill_route_constraint_stacked() -> anyhow::Result<()> { let buy_4 = limit_buy(pair_3.clone(), 1u64.into(), price3100); let buy_5 = limit_buy(pair_3.clone(), 1u64.into(), price10000); - state_tx.put_position(buy_1).await.unwrap(); - state_tx.put_position(buy_2).await.unwrap(); - state_tx.put_position(buy_3).await.unwrap(); - state_tx.put_position(buy_4).await.unwrap(); - state_tx.put_position(buy_5).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); + state_tx.open_position(buy_2).await.unwrap(); + state_tx.open_position(buy_3).await.unwrap(); + state_tx.open_position(buy_4).await.unwrap(); + state_tx.open_position(buy_5).await.unwrap(); let delta_1 = Value { asset_id: gm.id(), @@ -678,7 +677,7 @@ async fn fill_route_constraint_1() -> anyhow::Result<()> { let price1 = one; let buy_1 = limit_buy(pair_1.clone(), 200u64.into(), price1); - state_tx.put_position(buy_1).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); /* pair 2 */ let price2 = Amount::from(2u64); @@ -688,10 +687,10 @@ async fn fill_route_constraint_1() -> anyhow::Result<()> { let buy_3 = limit_buy(pair_2.clone(), 50u64.into(), price2); let buy_4 = limit_buy(pair_2.clone(), 50u64.into(), price2); - state_tx.put_position(buy_1).await.unwrap(); - state_tx.put_position(buy_2).await.unwrap(); - state_tx.put_position(buy_3).await.unwrap(); - state_tx.put_position(buy_4).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); + state_tx.open_position(buy_2).await.unwrap(); + state_tx.open_position(buy_3).await.unwrap(); + state_tx.open_position(buy_4).await.unwrap(); /* pair 3 */ let price2000 = 2000u64.into(); @@ -706,11 +705,11 @@ async fn fill_route_constraint_1() -> anyhow::Result<()> { let buy_4 = limit_buy(pair_3.clone(), 1u64.into(), price3100); let buy_5 = limit_buy(pair_3.clone(), 1u64.into(), price10000); - state_tx.put_position(buy_1).await.unwrap(); - state_tx.put_position(buy_2).await.unwrap(); - state_tx.put_position(buy_3).await.unwrap(); - state_tx.put_position(buy_4).await.unwrap(); - state_tx.put_position(buy_5).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); + state_tx.open_position(buy_2).await.unwrap(); + state_tx.open_position(buy_3).await.unwrap(); + state_tx.open_position(buy_4).await.unwrap(); + state_tx.open_position(buy_5).await.unwrap(); let delta_1 = Value { asset_id: gm.id(), @@ -782,13 +781,13 @@ async fn fill_route_unconstrained() -> anyhow::Result<()> { let price1 = one; let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); let buy_2 = limit_buy(pair_1.clone(), 1u64.into(), price1); - state_tx.put_position(buy_1).await.unwrap(); - state_tx.put_position(buy_2).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); + state_tx.open_position(buy_2).await.unwrap(); let buy_1 = limit_buy(pair_2.clone(), 1u64.into(), price1); let buy_2 = limit_buy(pair_2.clone(), 1u64.into(), price1); - state_tx.put_position(buy_1).await.unwrap(); - state_tx.put_position(buy_2).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); + state_tx.open_position(buy_2).await.unwrap(); let price1500 = 1500u64.into(); let buy_1 = limit_buy(pair_3.clone(), 1u64.into(), price1500); @@ -796,11 +795,11 @@ async fn fill_route_unconstrained() -> anyhow::Result<()> { let buy_3 = limit_buy(pair_3.clone(), 1u64.into(), price1500); let buy_4 = limit_buy(pair_3.clone(), 1u64.into(), price1500); let buy_5 = limit_buy(pair_3.clone(), 1u64.into(), price1500); - state_tx.put_position(buy_1).await.unwrap(); - state_tx.put_position(buy_2).await.unwrap(); - state_tx.put_position(buy_3).await.unwrap(); - state_tx.put_position(buy_4).await.unwrap(); - state_tx.put_position(buy_5).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); + state_tx.open_position(buy_2).await.unwrap(); + state_tx.open_position(buy_3).await.unwrap(); + state_tx.open_position(buy_4).await.unwrap(); + state_tx.open_position(buy_5).await.unwrap(); let delta_1 = Value { asset_id: gm.id(), @@ -876,15 +875,15 @@ async fn fill_route_hit_spill_price() -> anyhow::Result<()> { let price1 = one; let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); let buy_2 = limit_buy(pair_1.clone(), 2u64.into(), price1); - state_tx.put_position(buy_1).await.unwrap(); - state_tx.put_position(buy_2).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); + state_tx.open_position(buy_2).await.unwrap(); let buy_1 = limit_buy(pair_2.clone(), one, price1); let buy_2 = limit_buy(pair_2.clone(), one, price1); let buy_3 = limit_buy(pair_2.clone(), one, price1); - state_tx.put_position(buy_1).await.unwrap(); - state_tx.put_position(buy_2).await.unwrap(); - state_tx.put_position(buy_3).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); + state_tx.open_position(buy_2).await.unwrap(); + state_tx.open_position(buy_3).await.unwrap(); let price1500 = Amount::from(1500u64); let price1400 = Amount::from(1400u64); @@ -893,9 +892,9 @@ async fn fill_route_hit_spill_price() -> anyhow::Result<()> { let buy_1 = limit_buy(pair_3.clone(), one, price1500); let buy_2 = limit_buy(pair_3.clone(), one, price1400); let buy_3 = limit_buy(pair_3.clone(), one, price1300); - state_tx.put_position(buy_1).await.unwrap(); - state_tx.put_position(buy_2).await.unwrap(); - state_tx.put_position(buy_3).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); + state_tx.open_position(buy_2).await.unwrap(); + state_tx.open_position(buy_3).await.unwrap(); let delta_1 = Value { asset_id: gm.id(), @@ -953,7 +952,7 @@ async fn simple_route() -> anyhow::Result<()> { // Create a single 1:1 gn:penumbra position (i.e. buy 1 gn at 1 penumbra). let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), 1u64.into()); - state_tx.put_position(buy_1).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); state_tx.apply(); // We should be able to call path_search and route through that position. @@ -988,20 +987,7 @@ async fn best_position_route_and_fill() -> anyhow::Result<()> { // Create a single 1:1 gn:penumbra position (i.e. buy 1 gn at 1 penumbra). let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), 1u64.into()); - state_tx.put_position(buy_1).await.unwrap(); - // TODO: later, this should be folded into an open_position method - state_tx - .vcb_credit(Value { - asset_id: gn.id(), - amount: Amount::from(1u64) * gn.unit_amount(), - }) - .await?; - state_tx - .vcb_credit(Value { - asset_id: penumbra.id(), - amount: Amount::from(1u64) * penumbra.unit_amount(), - }) - .await?; + state_tx.open_position(buy_1).await.unwrap(); state_tx.apply(); // We should be able to call path_search and route through that position. @@ -1077,27 +1063,6 @@ async fn multi_hop_route_and_fill() -> anyhow::Result<()> { let pair_gn_gm = DirectedUnitPair::new(gn.clone(), gm.clone()); let pair_gm_penumbra = DirectedUnitPair::new(gm.clone(), penumbra.clone()); - // TEMP TODO: disable VCB for this test. Later, remove this code once we restructure - // the position manager. - let infinite_gm = Value { - asset_id: gm.id(), - amount: Amount::from(100000u128) * gm.unit_amount(), - }; - - let infinite_gn = Value { - asset_id: gn.id(), - amount: Amount::from(100000u128) * gn.unit_amount(), - }; - - let infinite_penumbra = Value { - asset_id: penumbra.id(), - amount: Amount::from(100000u128) * penumbra.unit_amount(), - }; - - state_tx.vcb_credit(infinite_gm).await?; - state_tx.vcb_credit(infinite_gn).await?; - state_tx.vcb_credit(infinite_penumbra).await?; - // Create a 2:1 penumbra:gm position (i.e. buy 20 gm at 2 penumbra each). let buy_1 = limit_buy_pq( pair_gm_penumbra.clone(), @@ -1106,7 +1071,7 @@ async fn multi_hop_route_and_fill() -> anyhow::Result<()> { 2u64.into(), 0u32, ); - state_tx.put_position(buy_1).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); // Create a 2.1:1 penumbra:gm position (i.e. buy 40 gm at 2.1 penumbra each). let buy_2 = limit_buy_pq( @@ -1116,7 +1081,7 @@ async fn multi_hop_route_and_fill() -> anyhow::Result<()> { 2100000u64.into(), 0u32, ); - state_tx.put_position(buy_2).await.unwrap(); + state_tx.open_position(buy_2).await.unwrap(); // Create a 2.2:1 penumbra:gm position (i.e. buy 160 gm at 2.2 penumbra each). let buy_3 = limit_buy_pq( @@ -1126,7 +1091,7 @@ async fn multi_hop_route_and_fill() -> anyhow::Result<()> { 2200000u64.into(), 0u32, ); - state_tx.put_position(buy_3).await.unwrap(); + state_tx.open_position(buy_3).await.unwrap(); // Create a 1:1 gm:gn position (i.e. buy 100 gm at 1 gn each). let buy_4 = limit_buy_pq( @@ -1137,7 +1102,7 @@ async fn multi_hop_route_and_fill() -> anyhow::Result<()> { // with 20bps fee 20u32, ); - state_tx.put_position(buy_4).await.unwrap(); + state_tx.open_position(buy_4).await.unwrap(); // Create a 1.9:1 penumbra:gn position (i.e. buy 160 gn at 1.9 penumbra each). let buy_5 = Position::new( @@ -1151,7 +1116,7 @@ async fn multi_hop_route_and_fill() -> anyhow::Result<()> { r2: 80000000u32.into(), }, ); - state_tx.put_position(buy_5).await.unwrap(); + state_tx.open_position(buy_5).await.unwrap(); // Create a 1:1 gm:gn position (i.e. buy 100 gn at 1 gm each). let buy_6 = limit_buy_pq( @@ -1162,7 +1127,7 @@ async fn multi_hop_route_and_fill() -> anyhow::Result<()> { // with 20bps fee 20u32, ); - state_tx.put_position(buy_6).await.unwrap(); + state_tx.open_position(buy_6).await.unwrap(); state_tx.apply(); @@ -1250,8 +1215,8 @@ async fn fill_dust_route() -> anyhow::Result<()> { let price1 = one; let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); let buy_2 = limit_buy(pair_1.clone(), 1u64.into(), price1); - state_tx.put_position(buy_1).await.unwrap(); - state_tx.put_position(buy_2).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); + state_tx.open_position(buy_2).await.unwrap(); let dust_constraint = Position::new( OsRng, pair_2.into_directed_trading_pair(), @@ -1263,7 +1228,7 @@ async fn fill_dust_route() -> anyhow::Result<()> { r2: 1u64.into(), }, ); - state_tx.put_position(dust_constraint).await.unwrap(); + state_tx.open_position(dust_constraint).await.unwrap(); let delta_1 = Value { asset_id: gm.id(), @@ -1315,8 +1280,8 @@ async fn fill_route_dust() { let price1 = one; let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); let buy_2 = limit_buy(pair_1.clone(), 1u64.into(), price1); - state_tx.put_position(buy_1).await.unwrap(); - state_tx.put_position(buy_2).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); + state_tx.open_position(buy_2).await.unwrap(); let dust_constraint = Position::new( OsRng, pair_2.into_directed_trading_pair(), @@ -1328,7 +1293,7 @@ async fn fill_route_dust() { r2: 1u64.into(), }, ); - state_tx.put_position(dust_constraint).await.unwrap(); + state_tx.open_position(dust_constraint).await.unwrap(); let delta_1 = Value { asset_id: gm.id(), @@ -1380,8 +1345,8 @@ async fn fill_route_with_dust_constraint() -> anyhow::Result<()> { let price1 = one; let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); let buy_2 = limit_buy(pair_1.clone(), 1u64.into(), price1); - state_tx.put_position(buy_1).await.unwrap(); - state_tx.put_position(buy_2).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); + state_tx.open_position(buy_2).await.unwrap(); let dust_constraint = Position::new( OsRng, @@ -1407,10 +1372,10 @@ async fn fill_route_with_dust_constraint() -> anyhow::Result<()> { }, ); - state_tx.put_position(dust_constraint).await.unwrap(); - state_tx.put_position(normal_order).await.unwrap(); + state_tx.open_position(dust_constraint).await.unwrap(); + state_tx.open_position(normal_order).await.unwrap(); let buy_1 = limit_buy(pair_3, 100u64.into(), 1400u64.into()); - state_tx.put_position(buy_1).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); let delta_1 = Value { asset_id: gm.id(), @@ -1468,8 +1433,8 @@ async fn fill_route_with_stacked_dust_constraint() -> anyhow::Result<()> { let price1 = one; let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); let buy_2 = limit_buy(pair_1.clone(), 1u64.into(), price1); - state_tx.put_position(buy_1).await.unwrap(); - state_tx.put_position(buy_2).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); + state_tx.open_position(buy_2).await.unwrap(); let dust_constraint_p2 = Position::new( OsRng, @@ -1495,8 +1460,8 @@ async fn fill_route_with_stacked_dust_constraint() -> anyhow::Result<()> { }, ); - state_tx.put_position(dust_constraint_p2).await.unwrap(); - state_tx.put_position(normal_order_p2).await.unwrap(); + state_tx.open_position(dust_constraint_p2).await.unwrap(); + state_tx.open_position(normal_order_p2).await.unwrap(); let dust_constraint_p3 = Position::new( OsRng, @@ -1522,11 +1487,11 @@ async fn fill_route_with_stacked_dust_constraint() -> anyhow::Result<()> { }, ); - state_tx.put_position(dust_constraint_p3).await.unwrap(); - state_tx.put_position(normal_order_p3).await.unwrap(); + state_tx.open_position(dust_constraint_p3).await.unwrap(); + state_tx.open_position(normal_order_p3).await.unwrap(); let buy_1 = limit_buy(pair_4, 100u64.into(), 1400u64.into()); - state_tx.put_position(buy_1).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); let delta_1 = Value { asset_id: gm.id(), @@ -1633,12 +1598,12 @@ async fn path_search_testnet_53_1_reproduction() -> anyhow::Result<()> { .unwrap() .into_position(OsRng); - state.put_position(s_a).await.unwrap(); - state.put_position(a_t).await.unwrap(); - state.put_position(s_b).await.unwrap(); - state.put_position(b_t).await.unwrap(); - state.put_position(s_c).await.unwrap(); - state.put_position(c_t).await.unwrap(); + state.open_position(s_a).await.unwrap(); + state.open_position(a_t).await.unwrap(); + state.open_position(s_b).await.unwrap(); + state.open_position(b_t).await.unwrap(); + state.open_position(s_c).await.unwrap(); + state.open_position(c_t).await.unwrap(); let cache = PathCache::begin(penumbra.id(), state.fork()); let mut cache_guard = cache.lock(); @@ -1801,15 +1766,15 @@ async fn path_search_commutative() -> anyhow::Result<()> { .unwrap() .into_position(OsRng); - state.put_position(s_a).await.unwrap(); - state.put_position(s_c).await.unwrap(); - state.put_position(a_b).await.unwrap(); - state.put_position(a_t).await.unwrap(); - state.put_position(b_t).await.unwrap(); - state.put_position(b_c).await.unwrap(); - state.put_position(c_t).await.unwrap(); - state.put_position(c_d).await.unwrap(); - state.put_position(d_t).await.unwrap(); + state.open_position(s_a).await.unwrap(); + state.open_position(s_c).await.unwrap(); + state.open_position(a_b).await.unwrap(); + state.open_position(a_t).await.unwrap(); + state.open_position(b_t).await.unwrap(); + state.open_position(b_c).await.unwrap(); + state.open_position(c_t).await.unwrap(); + state.open_position(c_d).await.unwrap(); + state.open_position(d_t).await.unwrap(); let cache = PathCache::begin(btc.id(), state.fork()); let mut cache_guard = cache.lock(); @@ -1993,10 +1958,10 @@ async fn path_search_unique() -> anyhow::Result<()> { .unwrap() .into_position(OsRng); - state.put_position(s_a).await.unwrap(); - state.put_position(s_b).await.unwrap(); - state.put_position(a_t).await.unwrap(); - state.put_position(b_t).await.unwrap(); + state.open_position(s_a).await.unwrap(); + state.open_position(s_b).await.unwrap(); + state.open_position(a_t).await.unwrap(); + state.open_position(b_t).await.unwrap(); let cache = PathCache::begin(pen.id(), state.fork()); let mut cache_guard = cache.lock(); diff --git a/crates/core/component/dex/src/component/tests.rs b/crates/core/component/dex/src/component/tests.rs index 1aca0cf75b..53353cd538 100644 --- a/crates/core/component/dex/src/component/tests.rs +++ b/crates/core/component/dex/src/component/tests.rs @@ -8,9 +8,6 @@ use penumbra_asset::{asset, Value}; use penumbra_num::Amount; use rand_core::OsRng; -//use crate::TempStorageExt; - -use crate::component::ValueCircuitBreaker as _; use crate::lp::action::PositionOpen; use crate::DexParameters; use crate::{ @@ -23,10 +20,6 @@ use crate::{ BatchSwapOutputData, DirectedTradingPair, DirectedUnitPair, }; -// TODO: what's the right way to mock genesis? if component A needs component B, -// do we need a way to mock B's genesis in A's tests? or should we only do unit -// tests for A, integration tests for A+B? - #[async_trait] pub trait TempStorageExt: Sized { async fn apply_minimal_genesis(self) -> anyhow::Result; @@ -83,7 +76,7 @@ async fn single_limit_order() -> anyhow::Result<()> { ); let position_1_id = position_1.id(); - state_tx.put_position(position_1.clone()).await.unwrap(); + state_tx.open_position(position_1.clone()).await.unwrap(); let mut state_test_1 = state_tx.fork(); @@ -333,9 +326,9 @@ async fn multiple_limit_orders() -> anyhow::Result<()> { let position_3_id = position_3.id(); // The insertion order shouldn't matter. - state_tx.put_position(position_2.clone()).await.unwrap(); - state_tx.put_position(position_1.clone()).await.unwrap(); - state_tx.put_position(position_3.clone()).await.unwrap(); + state_tx.open_position(position_2.clone()).await.unwrap(); + state_tx.open_position(position_1.clone()).await.unwrap(); + state_tx.open_position(position_3.clone()).await.unwrap(); let mut full_orderbook_state = state_tx; @@ -501,7 +494,7 @@ async fn position_create_and_retrieve() -> anyhow::Result<()> { r2: Amount::from(1u64) * price1 * gn.clone().unit_amount(), }, ); - state_tx.put_position(buy_1.clone()).await.unwrap(); + state_tx.open_position(buy_1.clone()).await.unwrap(); state_tx.apply(); let stream = state.all_positions(); @@ -526,7 +519,7 @@ async fn position_create_and_retrieve() -> anyhow::Result<()> { r2: Amount::from(1u64) * price2 * gn.clone().unit_amount(), }, ); - state_tx.put_position(buy_2.clone()).await.unwrap(); + state_tx.open_position(buy_2.clone()).await.unwrap(); state_tx.apply(); let stream = state.all_positions(); @@ -561,24 +554,9 @@ async fn swap_execution_tests() -> anyhow::Result<()> { let pair_gn_penumbra = DirectedUnitPair::new(gn.clone(), penumbra.clone()); - // TEMP TODO: disable VCB for this test. Later, remove this code once we restructure - // the position manager. - let infinite_gn = Value { - asset_id: gn.id(), - amount: Amount::from(100000u128) * gn.unit_amount(), - }; - - let infinite_penumbra = Value { - asset_id: penumbra.id(), - amount: Amount::from(100000u128) * penumbra.unit_amount(), - }; - - state_tx.vcb_credit(infinite_gn).await?; - state_tx.vcb_credit(infinite_penumbra).await?; - // Create a single 1:1 gn:penumbra position (i.e. buy 1 gn at 1 penumbra). let buy_1 = limit_buy(pair_gn_penumbra.clone(), 1u64.into(), 1u64.into()); - state_tx.put_position(buy_1).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); state_tx.apply(); // Now we should be able to fill a 1:1 gn:penumbra swap. @@ -643,30 +621,6 @@ async fn swap_execution_tests() -> anyhow::Result<()> { .get_unit("test_usd") .unwrap(); - // TEMP TODO: disable VCB for this test. Later, remove this code once we restructure - // the position manager. - let infinite_gn = Value { - asset_id: gn.id(), - amount: Amount::from(100000u128) * gn.unit_amount(), - }; - let infinite_gm = Value { - asset_id: gm.id(), - amount: Amount::from(100000u128) * gm.unit_amount(), - }; - let infinite_penumbra = Value { - asset_id: penumbra.id(), - amount: Amount::from(100000u128) * penumbra.unit_amount(), - }; - let infinite_pusd = Value { - asset_id: pusd.id(), - amount: Amount::from(100000u128) * pusd.unit_amount(), - }; - - state_tx.vcb_credit(infinite_gn).await?; - state_tx.vcb_credit(infinite_gm).await?; - state_tx.vcb_credit(infinite_penumbra).await?; - state_tx.vcb_credit(infinite_pusd).await?; - tracing::info!(gm_id = ?gm.id()); tracing::info!(gn_id = ?gn.id()); tracing::info!(pusd_id = ?pusd.id()); @@ -676,7 +630,7 @@ async fn swap_execution_tests() -> anyhow::Result<()> { // Sell 25 gn at 1 gm each. state_tx - .put_position(limit_sell( + .open_position(limit_sell( DirectedUnitPair::new(gn.clone(), gm.clone()), 25u64.into(), 1u64.into(), @@ -685,7 +639,7 @@ async fn swap_execution_tests() -> anyhow::Result<()> { .unwrap(); // Buy 1 pusd at 20 gm each. state_tx - .put_position(limit_buy( + .open_position(limit_buy( DirectedUnitPair::new(pusd.clone(), gm.clone()), 1u64.into(), 20u64.into(), @@ -694,7 +648,7 @@ async fn swap_execution_tests() -> anyhow::Result<()> { .unwrap(); // Buy 5 penumbra at 1 gm each. state_tx - .put_position(limit_buy( + .open_position(limit_buy( DirectedUnitPair::new(penumbra.clone(), gm.clone()), 5u64.into(), 1u64.into(), @@ -703,7 +657,7 @@ async fn swap_execution_tests() -> anyhow::Result<()> { .unwrap(); // Sell 1pusd at 5 penumbra each. state_tx - .put_position(limit_sell( + .open_position(limit_sell( DirectedUnitPair::new(pusd.clone(), penumbra.clone()), 1u64.into(), 5u64.into(), @@ -804,7 +758,7 @@ async fn basic_cycle_arb() -> anyhow::Result<()> { // Sell 10 gn at 1 penumbra each. state_tx - .put_position(limit_sell( + .open_position(limit_sell( DirectedUnitPair::new(gn.clone(), penumbra.clone()), 10u64.into(), 1u64.into(), @@ -813,7 +767,7 @@ async fn basic_cycle_arb() -> anyhow::Result<()> { .unwrap(); // Buy 100 gn at 2 gm each. state_tx - .put_position(limit_buy( + .open_position(limit_buy( DirectedUnitPair::new(gn.clone(), gm.clone()), 100u64.into(), 2u64.into(), @@ -822,7 +776,7 @@ async fn basic_cycle_arb() -> anyhow::Result<()> { .unwrap(); // Sell 100 penumbra at 1 gm each. state_tx - .put_position(limit_sell( + .open_position(limit_sell( DirectedUnitPair::new(penumbra.clone(), gm.clone()), 100u64.into(), 1u64.into(), @@ -915,9 +869,9 @@ async fn reproduce_arbitrage_loop_testnet_53() -> anyhow::Result<()> { let mut sell_1 = limit_sell(penumbra_usd.clone(), 10u64.into(), 100u64.into()); sell_1.nonce = [0; 32]; - state_tx.put_position(buy_1).await.unwrap(); - state_tx.put_position(buy_2).await.unwrap(); - state_tx.put_position(sell_1).await.unwrap(); + state_tx.open_position(buy_1).await.unwrap(); + state_tx.open_position(buy_2).await.unwrap(); + state_tx.open_position(sell_1).await.unwrap(); state_tx.apply(); diff --git a/crates/core/component/dex/src/event.rs b/crates/core/component/dex/src/event.rs index 660efc6be4..22832b2bf9 100644 --- a/crates/core/component/dex/src/event.rs +++ b/crates/core/component/dex/src/event.rs @@ -1,6 +1,6 @@ use crate::{ lp::{ - action::{PositionClose, PositionOpen, PositionWithdraw}, + action::PositionClose, position::{self, Position}, }, swap::Swap, @@ -30,13 +30,13 @@ pub fn swap_claim(swap_claim: &SwapClaim) -> pb::EventSwapClaim { } } -pub fn position_open(position_open: &PositionOpen) -> pb::EventPositionOpen { +pub fn position_open(position: &Position) -> pb::EventPositionOpen { pb::EventPositionOpen { - position_id: Some(position_open.position.id().into()), - trading_pair: Some(position_open.position.phi.pair.into()), - reserves_1: Some(position_open.position.reserves.r1.into()), - reserves_2: Some(position_open.position.reserves.r2.into()), - trading_fee: position_open.position.phi.component.fee, + position_id: Some(position.id().into()), + trading_pair: Some(position.phi.pair.into()), + reserves_1: Some(position.reserves.r1.into()), + reserves_2: Some(position.reserves.r2.into()), + trading_fee: position.phi.component.fee, } } @@ -49,7 +49,7 @@ pub fn position_close(action: &PositionClose) -> pb::EventPositionClose { } pub fn position_withdraw( - position_withdraw: &PositionWithdraw, + position_id: position::Id, final_position_state: &Position, ) -> pb::EventPositionWithdraw { let sequence = if let position::State::Withdrawn { sequence, .. } = final_position_state.state { @@ -58,7 +58,7 @@ pub fn position_withdraw( 0 }; pb::EventPositionWithdraw { - position_id: Some(position_withdraw.position_id.into()), + position_id: Some(position_id.into()), trading_pair: Some(final_position_state.phi.pair.into()), reserves_1: Some(final_position_state.reserves.r1.into()), reserves_2: Some(final_position_state.reserves.r2.into()), @@ -66,7 +66,7 @@ pub fn position_withdraw( } } -pub fn position_execution(post_execution_state: Position) -> pb::EventPositionExecution { +pub fn position_execution(post_execution_state: &Position) -> pb::EventPositionExecution { pb::EventPositionExecution { position_id: Some(post_execution_state.id().into()), trading_pair: Some(post_execution_state.phi.pair.into()), From 7a9627878a68c3f72f7d7c755133e05d19c191fd Mon Sep 17 00:00:00 2001 From: Henry de Valence Date: Mon, 25 Mar 2024 22:12:47 -0700 Subject: [PATCH 025/168] dex: rename put_position=>update_position --- .../component/dex/src/component/position_manager.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/core/component/dex/src/component/position_manager.rs b/crates/core/component/dex/src/component/position_manager.rs index d6d8d268c8..a9b1390b57 100644 --- a/crates/core/component/dex/src/component/position_manager.rs +++ b/crates/core/component/dex/src/component/position_manager.rs @@ -152,7 +152,7 @@ pub trait PositionManager: StateWrite + PositionRead { tracing::debug!(?id, "position found, close it"); position.state = position::State::Closed; - self.put_position(position).await?; + self.update_position(position).await?; Ok(()) } @@ -196,7 +196,7 @@ pub trait PositionManager: StateWrite + PositionRead { // Finally, record the new position state. self.record_proto(event::position_open(&position)); - self.put_position(position).await?; + self.update_position(position).await?; Ok(()) } @@ -205,7 +205,7 @@ pub trait PositionManager: StateWrite + PositionRead { #[tracing::instrument(level = "debug", skip_all)] async fn position_execution(&mut self, post_execution_state: position::Position) -> Result<()> { self.record_proto(event::position_execution(&post_execution_state)); - self.put_position(post_execution_state).await?; + self.update_position(post_execution_state).await?; Ok(()) } @@ -282,7 +282,7 @@ pub trait PositionManager: StateWrite + PositionRead { }; metadata.reserves = Reserves::zero(); - self.put_position(metadata).await?; + self.update_position(metadata).await?; Ok(reserves) } @@ -297,7 +297,7 @@ pub(crate) trait Inner: StateWrite { /// This should be the SOLE ENTRYPOINT for writing positions to the state. /// All other position changes exposed by the `PositionManager` should run through here. #[tracing::instrument(level = "debug", skip(self, position), fields(id = ?position.id()))] - async fn put_position(&mut self, position: position::Position) -> Result<()> { + async fn update_position(&mut self, position: position::Position) -> Result<()> { let id = position.id(); tracing::debug!(?position, "fetch position's previous state from storage"); // We pull the position from the state unconditionally, since we will From 20588573b1263c7ae0deefcd7d7404d26ddd3492 Mon Sep 17 00:00:00 2001 From: Henry de Valence Date: Mon, 25 Mar 2024 22:57:07 -0700 Subject: [PATCH 026/168] dex: restore close-on-fill, safely --- .../dex/src/component/position_manager.rs | 21 ++++++-- .../core/component/dex/src/component/tests.rs | 54 +++++++++++++++++++ crates/core/component/dex/src/lp/position.rs | 2 +- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/crates/core/component/dex/src/component/position_manager.rs b/crates/core/component/dex/src/component/position_manager.rs index a9b1390b57..e4d33161a3 100644 --- a/crates/core/component/dex/src/component/position_manager.rs +++ b/crates/core/component/dex/src/component/position_manager.rs @@ -203,9 +203,24 @@ pub trait PositionManager: StateWrite + PositionRead { /// Record execution against an opened position. #[tracing::instrument(level = "debug", skip_all)] - async fn position_execution(&mut self, post_execution_state: position::Position) -> Result<()> { - self.record_proto(event::position_execution(&post_execution_state)); - self.update_position(post_execution_state).await?; + async fn position_execution(&mut self, mut position: Position) -> Result<()> { + // Handle "close-on-fill": automatically flip the position state to "closed" if + // either of the reserves are zero. + if position.close_on_fill { + if position.reserves.r1 == 0u64.into() || position.reserves.r2 == 0u64.into() { + tracing::debug!( + id = ?position.id(), + r1 = ?position.reserves.r1, + r2 = ?position.reserves.r2, + "marking position as closed due to close-on-fill" + ); + position.state = position::State::Closed; + } + } + + self.record_proto(event::position_execution(&position)); + self.update_position(position).await?; + Ok(()) } diff --git a/crates/core/component/dex/src/component/tests.rs b/crates/core/component/dex/src/component/tests.rs index 53353cd538..69baee6c9b 100644 --- a/crates/core/component/dex/src/component/tests.rs +++ b/crates/core/component/dex/src/component/tests.rs @@ -9,6 +9,7 @@ use penumbra_num::Amount; use rand_core::OsRng; use crate::lp::action::PositionOpen; +use crate::lp::{position, SellOrder}; use crate::DexParameters; use crate::{ component::{ @@ -257,6 +258,59 @@ async fn single_limit_order() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +/// Builds a simple order book with a two orders, fills against them both, +/// and checks that one of the orders is auto-closed. +async fn check_close_on_fill() -> anyhow::Result<()> { + let storage = TempStorage::new().await?.apply_minimal_genesis().await?; + let mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); + let mut state_tx = state.try_begin_transaction().unwrap(); + + let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap(); + + let mut position_1 = SellOrder::parse_str("100gm@1gn")?.into_position(OsRng); + position_1.close_on_fill = true; + let position_2 = SellOrder::parse_str("100gm@1.1gn")?.into_position(OsRng); + + let position_1_id = position_1.id(); + let position_2_id = position_2.id(); + + state_tx.open_position(position_1.clone()).await.unwrap(); + state_tx.open_position(position_2.clone()).await.unwrap(); + + // Now we have the following liquidity: + // + // 100gm@1gn (auto-closing) + // 100gm@1.1gn + // + // We therefore expect that trading 100gn + 110gn will exhaust both positions. + // Attempting to trade a bit more than that ensures we completely fill both, + // without worrying about rounding. + // Because we're just testing the DEX internals, we need to trigger fill_route manually. + let input = "220gn".parse::().unwrap(); + let route = [gm.id()]; + let execution = FillRoute::fill_route(&mut state_tx, input, &route, None).await?; + + let unfilled = input.amount.checked_sub(&execution.input.amount).unwrap(); + + // Check that we got the execution we expected. + assert_eq!(unfilled, "10gn".parse::().unwrap().amount); + assert_eq!(execution.output, "200gm".parse::().unwrap()); + + // Now grab both position states: + let position_1_post_exec = state_tx.position_by_id(&position_1_id).await?.unwrap(); + let position_2_post_exec = state_tx.position_by_id(&position_2_id).await?.unwrap(); + + dbg!(&position_1_post_exec); + dbg!(&position_2_post_exec); + + // Check that position 1 was auto-closed but position 2 wasn't: + assert_eq!(position_1_post_exec.state, position::State::Closed); + assert_eq!(position_2_post_exec.state, position::State::Opened); + + Ok(()) +} + #[tokio::test] /// Try to execute against multiple positions, mainly testing that the order-book traversal /// is done correctly. diff --git a/crates/core/component/dex/src/lp/position.rs b/crates/core/component/dex/src/lp/position.rs index e6aae9c636..05dabc0d74 100644 --- a/crates/core/component/dex/src/lp/position.rs +++ b/crates/core/component/dex/src/lp/position.rs @@ -33,7 +33,7 @@ pub struct Position { /// sequence of stateful NFTs based on the [`Id`]. pub nonce: [u8; 32], /// Set to `true` if a position is a limit-order, meaning that it will be closed after being - /// filled against. Note that this is not currently supported in the dex state machine. + /// filled against. pub close_on_fill: bool, } From 83f5311ff9974d3805989c059016284b30230e04 Mon Sep 17 00:00:00 2001 From: Ava Howell Date: Tue, 26 Mar 2024 12:30:03 -0700 Subject: [PATCH 027/168] ibc: use outbound_ics20_transfers_enabled chain parameter (#4101) this uses the existing outbound_ics20_transfers_enabled to gate ics20 withdrawals --- crates/core/app/src/action_handler/actions.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/core/app/src/action_handler/actions.rs b/crates/core/app/src/action_handler/actions.rs index 6262b7382d..4bc74f331e 100644 --- a/crates/core/app/src/action_handler/actions.rs +++ b/crates/core/app/src/action_handler/actions.rs @@ -84,7 +84,18 @@ impl AppActionHandler for Action { .check_stateful(state) .await } - Action::Ics20Withdrawal(action) => action.check_historical(state).await, + Action::Ics20Withdrawal(action) => { + // SAFETY: this is safe to check in parallel because IBC enablement cannot + // change during transaction execution. + if !state + .get_ibc_params() + .await? + .outbound_ics20_transfers_enabled + { + anyhow::bail!("transaction an ICS20 withdrawal, but outbound ICS20 withdrawals are not enabled"); + } + action.check_historical(state).await + } Action::CommunityPoolSpend(action) => action.check_historical(state).await, Action::CommunityPoolOutput(action) => action.check_historical(state).await, Action::CommunityPoolDeposit(action) => action.check_historical(state).await, From 6aa6efab4c1af3d9ef7626e0e3a8ddc8b7dc51b2 Mon Sep 17 00:00:00 2001 From: Conor Schaefer Date: Tue, 26 Mar 2024 12:35:23 -0700 Subject: [PATCH 028/168] chore: release version 0.70.0 --- Cargo.lock | 88 +++++++++++++++--------------- Cargo.toml | 2 +- docs/guide/src/penumbra_version.md | 2 +- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be1b12c5b8..a012391469 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1378,7 +1378,7 @@ dependencies = [ [[package]] name = "cnidarium" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "async-trait", @@ -1413,7 +1413,7 @@ dependencies = [ [[package]] name = "cnidarium-component" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "async-trait", @@ -1892,7 +1892,7 @@ dependencies = [ [[package]] name = "decaf377-fmd" -version = "0.69.0" +version = "0.70.0" dependencies = [ "ark-ff", "ark-serialize", @@ -1907,7 +1907,7 @@ dependencies = [ [[package]] name = "decaf377-frost" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ff", @@ -1922,7 +1922,7 @@ dependencies = [ [[package]] name = "decaf377-ka" -version = "0.69.0" +version = "0.70.0" dependencies = [ "ark-ff", "decaf377 0.5.0", @@ -4397,7 +4397,7 @@ dependencies = [ [[package]] name = "pcli" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ff", @@ -4475,7 +4475,7 @@ dependencies = [ [[package]] name = "pclientd" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "assert_cmd", @@ -4527,7 +4527,7 @@ dependencies = [ [[package]] name = "pd" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ff", @@ -4666,7 +4666,7 @@ dependencies = [ [[package]] name = "penumbra-app" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ff", @@ -4746,7 +4746,7 @@ dependencies = [ [[package]] name = "penumbra-asset" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ff", @@ -4785,7 +4785,7 @@ dependencies = [ [[package]] name = "penumbra-auto-https" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "axum-server", @@ -4797,7 +4797,7 @@ dependencies = [ [[package]] name = "penumbra-bench" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ec", @@ -4841,7 +4841,7 @@ dependencies = [ [[package]] name = "penumbra-community-pool" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ff", @@ -4873,7 +4873,7 @@ dependencies = [ [[package]] name = "penumbra-compact-block" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ff", @@ -4909,7 +4909,7 @@ dependencies = [ [[package]] name = "penumbra-custody" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ff", @@ -4942,7 +4942,7 @@ dependencies = [ [[package]] name = "penumbra-dex" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ff", @@ -5002,7 +5002,7 @@ dependencies = [ [[package]] name = "penumbra-distributions" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "async-trait", @@ -5019,7 +5019,7 @@ dependencies = [ [[package]] name = "penumbra-eddy" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ff", @@ -5037,7 +5037,7 @@ dependencies = [ [[package]] name = "penumbra-fee" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ff", @@ -5062,7 +5062,7 @@ dependencies = [ [[package]] name = "penumbra-funding" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "async-trait", @@ -5084,7 +5084,7 @@ dependencies = [ [[package]] name = "penumbra-genesis" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "penumbra-community-pool", @@ -5103,7 +5103,7 @@ dependencies = [ [[package]] name = "penumbra-governance" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ff", @@ -5160,7 +5160,7 @@ dependencies = [ [[package]] name = "penumbra-ibc" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ff", @@ -5195,7 +5195,7 @@ dependencies = [ [[package]] name = "penumbra-keys" -version = "0.69.0" +version = "0.70.0" dependencies = [ "aes 0.8.4", "anyhow", @@ -5241,7 +5241,7 @@ dependencies = [ [[package]] name = "penumbra-measure" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "bytesize", @@ -5260,7 +5260,7 @@ dependencies = [ [[package]] name = "penumbra-mock-client" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "cnidarium", @@ -5276,7 +5276,7 @@ dependencies = [ [[package]] name = "penumbra-mock-consensus" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "bytes", @@ -5291,7 +5291,7 @@ dependencies = [ [[package]] name = "penumbra-num" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ff", @@ -5328,7 +5328,7 @@ dependencies = [ [[package]] name = "penumbra-proof-params" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ec", @@ -5357,7 +5357,7 @@ dependencies = [ [[package]] name = "penumbra-proof-setup" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ec", @@ -5385,7 +5385,7 @@ dependencies = [ [[package]] name = "penumbra-proto" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "async-trait", @@ -5415,7 +5415,7 @@ dependencies = [ [[package]] name = "penumbra-sct" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ff", @@ -5448,7 +5448,7 @@ dependencies = [ [[package]] name = "penumbra-shielded-pool" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ff", @@ -5498,7 +5498,7 @@ dependencies = [ [[package]] name = "penumbra-stake" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ff", @@ -5551,7 +5551,7 @@ dependencies = [ [[package]] name = "penumbra-tct" -version = "0.69.0" +version = "0.70.0" dependencies = [ "ark-ed-on-bls12-377", "ark-ff", @@ -5582,7 +5582,7 @@ dependencies = [ [[package]] name = "penumbra-tct-property-test" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "futures", @@ -5594,7 +5594,7 @@ dependencies = [ [[package]] name = "penumbra-tct-visualize" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "axum", @@ -5624,7 +5624,7 @@ dependencies = [ [[package]] name = "penumbra-tendermint-proxy" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "chrono", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "penumbra-tower-trace" -version = "0.69.0" +version = "0.70.0" dependencies = [ "futures", "hex", @@ -5676,7 +5676,7 @@ dependencies = [ [[package]] name = "penumbra-transaction" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-ff", @@ -5728,7 +5728,7 @@ dependencies = [ [[package]] name = "penumbra-txhash" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "blake2b_simd 1.0.2", @@ -5740,7 +5740,7 @@ dependencies = [ [[package]] name = "penumbra-view" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-std", @@ -5795,7 +5795,7 @@ dependencies = [ [[package]] name = "penumbra-wallet" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-std", @@ -7607,7 +7607,7 @@ checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" [[package]] name = "summonerd" -version = "0.69.0" +version = "0.70.0" dependencies = [ "anyhow", "ark-groth16", diff --git a/Cargo.toml b/Cargo.toml index 8e311e2da0..252042fdc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,7 +98,7 @@ push = false [workspace.package] authors = ["Penumbra Labs "] edition = "2021" -version = "0.69.0" +version = "0.70.0" repository = "https://github.com/penumbra-zone/penumbra" homepage = "https://penumbra.zone" license = "MIT OR Apache-2.0" diff --git a/docs/guide/src/penumbra_version.md b/docs/guide/src/penumbra_version.md index 028cafdb6c..9ecab20b77 100644 --- a/docs/guide/src/penumbra_version.md +++ b/docs/guide/src/penumbra_version.md @@ -1 +1 @@ -v0.69.1 +v0.70.0 From 26a82e0418728302ff75e66d4bc7102a24dd554f Mon Sep 17 00:00:00 2001 From: plaidfinch Date: Tue, 26 Mar 2024 22:33:29 +0000 Subject: [PATCH 029/168] Support threshold custody for validators (#4037) This PR finishes #3813, adding support for signing validator definitions and votes to the threshold custody backend, as well as widening the general interface for custody backends to permit any backend to support these operations. **Status:** @cronokirby and I walked through this and determined that it does work after all, the UX is just a little unfortunate when multiple calls to threshold custody backend are required to broadcast a single transaction (as in the case with validator custody). But it works! --------- Co-authored-by: finch Co-authored-by: Lucas Meier --- Cargo.lock | 2 + crates/bin/pcli/src/command/threshold.rs | 18 +- crates/bin/pcli/src/command/validator.rs | 82 +--- crates/bin/pcli/src/main.rs | 1 + crates/bin/pcli/src/network.rs | 44 +- crates/bin/pcli/src/opt.rs | 27 +- crates/bin/pcli/src/terminal.rs | 20 +- .../src/gen/proto_descriptor.bin.no_lfs | Bin 88631 -> 98383 bytes crates/custody/Cargo.toml | 2 + crates/custody/src/lib.rs | 4 +- crates/custody/src/null_kms.rs | 18 + crates/custody/src/policy.rs | 152 +++++-- crates/custody/src/pre_auth.rs | 7 +- crates/custody/src/request.rs | 88 ++++ crates/custody/src/soft_kms.rs | 101 ++++- crates/custody/src/threshold.rs | 147 ++++-- crates/custody/src/threshold/sign.rs | 295 +++++++++--- .../src/gen/penumbra.custody.threshold.v1.rs | 27 +- .../penumbra.custody.threshold.v1.serde.rs | 47 +- crates/proto/src/gen/penumbra.custody.v1.rs | 263 +++++++++++ .../src/gen/penumbra.custody.v1.serde.rs | 420 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 366417 -> 379528 bytes .../custody/threshold/v1/threshold.proto | 12 +- .../penumbra/custody/v1/custody.proto | 45 ++ 24 files changed, 1576 insertions(+), 246 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a012391469..a93223ca59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4925,8 +4925,10 @@ dependencies = [ "ed25519-consensus", "futures", "hex", + "penumbra-governance", "penumbra-keys", "penumbra-proto", + "penumbra-stake", "penumbra-transaction", "penumbra-txhash", "prost", diff --git a/crates/bin/pcli/src/command/threshold.rs b/crates/bin/pcli/src/command/threshold.rs index a7923acf05..41c3027cac 100644 --- a/crates/bin/pcli/src/command/threshold.rs +++ b/crates/bin/pcli/src/command/threshold.rs @@ -1,6 +1,10 @@ use anyhow::Result; -use crate::{terminal::ActualTerminal, App}; +use crate::{ + config::{CustodyConfig, GovernanceCustodyConfig}, + terminal::ActualTerminal, + App, +}; #[derive(Debug, clap::Subcommand)] pub enum ThresholdCmd { @@ -18,12 +22,18 @@ impl ThresholdCmd { #[tracing::instrument(skip(self, app))] pub async fn exec(&self, app: &mut App) -> Result<()> { let config = match &app.config.custody { - crate::config::CustodyConfig::Threshold(config) => config, - _ => anyhow::bail!("this command can only be used with the threshold custody backend"), + CustodyConfig::Threshold(config) => Some(config), + _ => None, // If not threshold, we can't sign using threshold config + }; + let governance_config = match &app.config.governance_custody { + Some(GovernanceCustodyConfig::Threshold(governance_config)) => Some(governance_config), + None => config, // If no governance config, use regular one + _ => None, // If not threshold, we can't sign using governance config }; match self { ThresholdCmd::Sign => { - penumbra_custody::threshold::follow(config, &ActualTerminal).await + penumbra_custody::threshold::follow(config, governance_config, &ActualTerminal) + .await } } } diff --git a/crates/bin/pcli/src/command/validator.rs b/crates/bin/pcli/src/command/validator.rs index 3cab716788..d1c0264457 100644 --- a/crates/bin/pcli/src/command/validator.rs +++ b/crates/bin/pcli/src/command/validator.rs @@ -14,9 +14,7 @@ use penumbra_governance::{ ValidatorVote, ValidatorVoteBody, ValidatorVoteReason, Vote, MAX_VALIDATOR_VOTE_REASON_LENGTH, }; use penumbra_keys::keys::AddressIndex; -use penumbra_proto::{ - core::component::stake::v1::Validator as ProtoValidator, DomainType, Message, -}; +use penumbra_proto::DomainType; use penumbra_stake::{ validator, validator::{Validator, ValidatorToml}, @@ -24,10 +22,7 @@ use penumbra_stake::{ }; use penumbra_wallet::plan; -use crate::{ - config::{CustodyConfig, GovernanceCustodyConfig}, - App, -}; +use crate::App; #[derive(Debug, clap::Subcommand)] pub enum ValidatorCmd { @@ -158,7 +153,6 @@ impl ValidatorCmd { } } - // TODO: move use of sk into custody service pub async fn exec(&self, app: &mut App) -> Result<()> { let fvk = app.config.full_viewing_key.clone(); @@ -195,18 +189,7 @@ impl ValidatorCmd { .file_name() .with_context(|| format!("invalid path: {file:?}"))?; - // TODO: use the custody abstraction to sign - let protobuf_serialized: ProtoValidator = new_validator.clone().into(); - let v_bytes = protobuf_serialized.encode_to_vec(); - let sk = match &app.config.custody { - CustodyConfig::SoftKms(config) => config.spend_key.clone(), - _ => { - anyhow::bail!( - "local validator definition signing currently requires SoftKMS backend" - ); - } - }; - let signature = sk.spend_auth_key().sign(OsRng, &v_bytes); + let signature = app.sign_validator_definition(new_validator.clone()).await?; if let Some(output_file) = signature_file { let output_file_path = std::fs::canonicalize(output_file) @@ -264,18 +247,7 @@ impl ValidatorCmd { ) .context("unable to parse decoded signature")? } else { - // TODO: use the custody abstraction to sign - let protobuf_serialized: ProtoValidator = new_validator.clone().into(); - let v_bytes = protobuf_serialized.encode_to_vec(); - let sk = match &app.config.custody { - CustodyConfig::SoftKms(config) => config.spend_key.clone(), - _ => { - anyhow::bail!( - "local validator definition signing currently requires SoftKMS backend" - ); - } - }; - sk.spend_auth_key().sign(OsRng, &v_bytes) + app.sign_validator_definition(new_validator.clone()).await? }; let vd = validator::Definition { validator: new_validator, @@ -322,28 +294,7 @@ impl ValidatorCmd { reason: ValidatorVoteReason(reason.clone()), }; - // TODO: use the custody abstraction to sign - let sk = match &app.config.governance_custody { - None => match &app.config.custody { - CustodyConfig::SoftKms(config) => config.spend_key.clone(), - _ => { - anyhow::bail!( - "local validator definition signing currently requires SoftKMS backend" - ); - } - }, - Some(GovernanceCustodyConfig::SoftKms(config)) => config.spend_key.clone(), - Some(_) => { - anyhow::bail!( - "local validator definition signing currently requires SoftKMS backend" - ); - } - }; - let governance_auth_key = sk.spend_auth_key(); - - // Generate an authorizing signature with the governance key for the vote body - let body_bytes = body.encode_to_vec(); - let signature = governance_auth_key.sign(OsRng, &body_bytes); + let signature = app.sign_validator_vote(body).await?; if let Some(signature_file) = signature_file { File::create(signature_file) @@ -407,28 +358,7 @@ impl ValidatorCmd { ) .context("unable to parse decoded signature")? } else { - // TODO: use the custody abstraction to sign - let sk = match &app.config.governance_custody { - None => match &app.config.custody { - CustodyConfig::SoftKms(config) => config.spend_key.clone(), - _ => { - anyhow::bail!( - "local validator definition signing currently requires SoftKMS backend" - ); - } - }, - Some(GovernanceCustodyConfig::SoftKms(config)) => config.spend_key.clone(), - Some(_) => { - anyhow::bail!( - "local validator definition signing currently requires SoftKMS backend" - ); - } - }; - let governance_auth_key = sk.spend_auth_key(); - - // Generate an authorizing signature with the governance key for the vote body - let body_bytes = body.encode_to_vec(); - governance_auth_key.sign(OsRng, &body_bytes) + app.sign_validator_vote(body.clone()).await? }; let vote = ValidatorVote { body, auth_sig }; diff --git a/crates/bin/pcli/src/main.rs b/crates/bin/pcli/src/main.rs index 004a6cb5ff..bfa17d64a3 100644 --- a/crates/bin/pcli/src/main.rs +++ b/crates/bin/pcli/src/main.rs @@ -36,6 +36,7 @@ pub struct App { /// correctly, this can be unwrapped safely. pub view: Option>, pub custody: CustodyServiceClient, + pub governance_custody: CustodyServiceClient, pub config: PcliConfig, } diff --git a/crates/bin/pcli/src/network.rs b/crates/bin/pcli/src/network.rs index ac2b17b220..d9f0502f67 100644 --- a/crates/bin/pcli/src/network.rs +++ b/crates/bin/pcli/src/network.rs @@ -1,11 +1,16 @@ use anyhow::Context; +use decaf377_rdsa::{Signature, SpendAuth}; use futures::{FutureExt, TryStreamExt}; use penumbra_fee::GasPrices; +use penumbra_governance::ValidatorVoteBody; use penumbra_proto::{ + custody::v1::{AuthorizeValidatorDefinitionRequest, AuthorizeValidatorVoteRequest}, util::tendermint_proxy::v1::tendermint_proxy_service_client::TendermintProxyServiceClient, view::v1::broadcast_transaction_response::Status as BroadcastStatus, - view::v1::GasPricesRequest, DomainType, + view::v1::GasPricesRequest, + DomainType, }; +use penumbra_stake::validator::Validator; use penumbra_transaction::{gas::GasCost, txhash::TransactionId, Transaction, TransactionPlan}; use penumbra_view::ViewClient; use std::future::Future; @@ -72,6 +77,43 @@ impl App { } } + pub async fn sign_validator_definition( + &mut self, + validator_definition: Validator, + ) -> anyhow::Result> { + let request = AuthorizeValidatorDefinitionRequest { + validator_definition: Some(validator_definition.into()), + pre_authorizations: vec![], + }; + self.custody + .authorize_validator_definition(request) + .await? + .into_inner() + .validator_definition_auth + .ok_or_else(|| anyhow::anyhow!("missing validator definition auth"))? + .try_into() + } + + pub async fn sign_validator_vote( + &mut self, + validator_vote: ValidatorVoteBody, + ) -> anyhow::Result> { + let request = AuthorizeValidatorVoteRequest { + validator_vote: Some(validator_vote.into()), + pre_authorizations: vec![], + }; + // Use the separate governance custody service, if one is configured, to sign the validator + // vote. This allows the governance custody service to have a different key than the main + // custody, which is useful for validators who want to have a separate key for voting. + self.governance_custody // VERY IMPORTANT: use governance custody here! + .authorize_validator_vote(request) + .await? + .into_inner() + .validator_vote_auth + .ok_or_else(|| anyhow::anyhow!("missing validator vote auth"))? + .try_into() + } + /// Submits a transaction to the network. pub async fn submit_transaction( &mut self, diff --git a/crates/bin/pcli/src/opt.rs b/crates/bin/pcli/src/opt.rs index 5e29a7b689..b9ce54a565 100644 --- a/crates/bin/pcli/src/opt.rs +++ b/crates/bin/pcli/src/opt.rs @@ -1,5 +1,5 @@ use crate::{ - config::{CustodyConfig, PcliConfig}, + config::{CustodyConfig, GovernanceCustodyConfig, PcliConfig}, terminal::ActualTerminal, App, Command, }; @@ -78,6 +78,30 @@ impl Opt { } }; + // Build the governance custody service... + let governance_custody = match &config.governance_custody { + Some(separate_governance_custody) => match separate_governance_custody { + GovernanceCustodyConfig::SoftKms(config) => { + tracing::info!( + "using separate software KMS custody service for validator voting" + ); + let soft_kms = SoftKms::new(config.clone()); + let custody_svc = CustodyServiceServer::new(soft_kms); + CustodyServiceClient::new(box_grpc_svc::local(custody_svc)) + } + GovernanceCustodyConfig::Threshold(config) => { + tracing::info!( + "using separate manual threshold custody service for validator voting" + ); + let threshold_kms = + penumbra_custody::threshold::Threshold::new(config.clone(), ActualTerminal); + let custody_svc = CustodyServiceServer::new(threshold_kms); + CustodyServiceClient::new(box_grpc_svc::local(custody_svc)) + } + }, + None => custody.clone(), // If no separate custody for validator voting, use the same one + }; + // ...and the view service... let view = match (self.cmd.offline(), &config.view_url) { // In offline mode, don't construct a view service at all. @@ -110,6 +134,7 @@ impl Opt { let app = App { view, custody, + governance_custody, config, }; Ok((app, self.cmd)) diff --git a/crates/bin/pcli/src/terminal.rs b/crates/bin/pcli/src/terminal.rs index 5238380b44..f98b273386 100644 --- a/crates/bin/pcli/src/terminal.rs +++ b/crates/bin/pcli/src/terminal.rs @@ -1,6 +1,5 @@ use anyhow::Result; -use penumbra_custody::threshold::Terminal; -use penumbra_transaction::TransactionPlan; +use penumbra_custody::threshold::{SigningRequest, Terminal}; use tokio::io::{self, AsyncBufReadExt}; use tonic::async_trait; @@ -11,9 +10,20 @@ pub struct ActualTerminal; #[async_trait] impl Terminal for ActualTerminal { - async fn confirm_transaction(&self, transaction: &TransactionPlan) -> Result { - println!("Do you approve this transaction?"); - println!("{}", serde_json::to_string_pretty(transaction)?); + async fn confirm_request(&self, signing_request: &SigningRequest) -> Result { + let (description, json) = match signing_request { + SigningRequest::TransactionPlan(plan) => { + ("transaction", serde_json::to_string_pretty(plan)?) + } + SigningRequest::ValidatorDefinition(def) => { + ("validator definition", serde_json::to_string_pretty(def)?) + } + SigningRequest::ValidatorVote(vote) => { + ("validator vote", serde_json::to_string_pretty(vote)?) + } + }; + println!("Do you approve this {description}?"); + println!("{json}"); println!("Press enter to continue"); self.next_response().await?; Ok(true) diff --git a/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs b/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs index c2c1f4df906c5e549e4bf4ff1514fc587b551e16..67d781308dc15349cc1633e0986b9be4d49fda1f 100644 GIT binary patch delta 26297 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 delta 16472 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 WQ+w4Mx1hSdx_-eQmpW|U1OEvTzRX+z diff --git a/crates/custody/Cargo.toml b/crates/custody/Cargo.toml index 83ec2958da..5d0a662a3b 100644 --- a/crates/custody/Cargo.toml +++ b/crates/custody/Cargo.toml @@ -18,8 +18,10 @@ decaf377-rdsa = {workspace = true} ed25519-consensus = {workspace = true} futures = {workspace = true} hex = {workspace = true} +penumbra-governance = {workspace = true, default-features = false} penumbra-keys = {workspace = true, default-features = true} penumbra-proto = {workspace = true, features = ["rpc"], default-features = true} +penumbra-stake = {workspace = true, default-features = false} penumbra-transaction = {workspace = true, default-features = true} penumbra-txhash = {workspace = true, default-features = true} prost = {workspace = true} diff --git a/crates/custody/src/lib.rs b/crates/custody/src/lib.rs index 0cf6bb39dc..fd48986a90 100644 --- a/crates/custody/src/lib.rs +++ b/crates/custody/src/lib.rs @@ -21,4 +21,6 @@ pub mod threshold; pub use client::CustodyClient; pub use pre_auth::PreAuthorization; -pub use request::AuthorizeRequest; +pub use request::{ + AuthorizeRequest, AuthorizeValidatorDefinitionRequest, AuthorizeValidatorVoteRequest, +}; diff --git a/crates/custody/src/null_kms.rs b/crates/custody/src/null_kms.rs index 402d8b63cc..907b511418 100644 --- a/crates/custody/src/null_kms.rs +++ b/crates/custody/src/null_kms.rs @@ -21,6 +21,24 @@ impl pb::custody_service_server::CustodyService for NullKms { )) } + async fn authorize_validator_definition( + &self, + _request: Request, + ) -> Result, Status> { + Err(tonic::Status::failed_precondition( + "Got authorization request in view-only mode to null KMS.", + )) + } + + async fn authorize_validator_vote( + &self, + _request: Request, + ) -> Result, Status> { + Err(tonic::Status::failed_precondition( + "Got authorization request in view-only mode to null KMS.", + )) + } + async fn export_full_viewing_key( &self, _request: Request, diff --git a/crates/custody/src/policy.rs b/crates/custody/src/policy.rs index 629f33cdc1..2253cc7f76 100644 --- a/crates/custody/src/policy.rs +++ b/crates/custody/src/policy.rs @@ -3,22 +3,47 @@ use std::collections::HashSet; use penumbra_keys::Address; +use penumbra_proto::{ + core::{ + component::{ + governance::v1::ValidatorVoteBody as ProtoValidatorVoteBody, + stake::v1::Validator as ProtoValidator, + }, + transaction::v1::TransactionPlan as ProtoTransactionPlan, + }, + Message as _, +}; use penumbra_transaction::plan::ActionPlan; use serde::{Deserialize, Serialize}; -use crate::{AuthorizeRequest, PreAuthorization}; +use crate::{ + AuthorizeRequest, AuthorizeValidatorDefinitionRequest, AuthorizeValidatorVoteRequest, + PreAuthorization, +}; /// A trait for checking whether a transaction plan is allowed by a policy. pub trait Policy { /// Checks whether the proposed transaction plan is allowed by this policy. - fn check(&self, request: &AuthorizeRequest) -> anyhow::Result<()>; + fn check_transaction(&self, request: &AuthorizeRequest) -> anyhow::Result<()>; + + /// Checks whether the proposed validator definition is allowed by this policy. + fn check_validator_definition( + &self, + _request: &AuthorizeValidatorDefinitionRequest, + ) -> anyhow::Result<()>; + + /// Checks whether the proposed validator vote is allowed by this policy. + fn check_validator_vote(&self, _request: &AuthorizeValidatorVoteRequest) -> anyhow::Result<()>; } /// A set of basic spend authorization policies. /// -/// These policies are intended to be simple enough that they can be written by -/// hand in a config file. More complex policy logic than than should be -/// implemented by a custom implementation of the [`Policy`] trait. +/// These policies are intended to be simple enough that they can be written by hand in a config +/// file. More complex policy logic than than should be implemented by a custom implementation of +/// the [`Policy`] trait. +/// +/// These policies do not permit validator votes or validator definition updates, so a custom policy +/// must be used to approve these actions. #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] #[serde(tag = "type")] pub enum AuthPolicy { @@ -58,6 +83,52 @@ pub enum PreAuthorizationPolicy { }, } +impl PreAuthorizationPolicy { + fn check_pre_authorizations( + &self, + pre_authorizations: &[PreAuthorization], + signed_data: impl AsRef<[u8]>, + ) -> anyhow::Result<()> { + let signed_data = signed_data.as_ref(); + match self { + PreAuthorizationPolicy::Ed25519 { + required_signatures, + allowed_signers, + } => { + #[allow(clippy::unnecessary_filter_map)] + let ed25519_pre_auths = + pre_authorizations + .iter() + .filter_map(|pre_auth| match pre_auth { + PreAuthorization::Ed25519(pre_auth) => Some(pre_auth), + // _ => None, + }); + + let mut allowed_signers = allowed_signers.iter().cloned().collect::>(); + let mut seen_signers = HashSet::new(); + + for pre_auth in ed25519_pre_auths { + // Remove the signer from the allowed signers set, so that + // each signer can only submit one pre-authorization. + if let Some(signer) = allowed_signers.take(&pre_auth.vk) { + pre_auth.verify(signed_data)?; + seen_signers.insert(signer); + } + } + + if seen_signers.len() < *required_signatures as usize { + anyhow::bail!( + "required {} pre-authorization signatures but only saw {}", + required_signatures, + seen_signers.len(), + ); + } + Ok(()) + } + } + } +} + mod address_as_string { use std::str::FromStr; @@ -130,7 +201,7 @@ mod ed25519_vec_base64 { } impl Policy for AuthPolicy { - fn check(&self, request: &AuthorizeRequest) -> anyhow::Result<()> { + fn check_transaction(&self, request: &AuthorizeRequest) -> anyhow::Result<()> { let plan = &request.plan; match self { AuthPolicy::DestinationAllowList { @@ -161,49 +232,44 @@ impl Policy for AuthPolicy { } Ok(()) } - AuthPolicy::PreAuthorization(policy) => policy.check(request), + AuthPolicy::PreAuthorization(policy) => policy.check_transaction(request), } } + + fn check_validator_definition( + &self, + _request: &AuthorizeValidatorDefinitionRequest, + ) -> anyhow::Result<()> { + anyhow::bail!("validator definitions are not allowed by this policy") + } + + fn check_validator_vote(&self, _request: &AuthorizeValidatorVoteRequest) -> anyhow::Result<()> { + anyhow::bail!("validator votes are not allowed by this policy") + } } impl Policy for PreAuthorizationPolicy { - fn check(&self, request: &AuthorizeRequest) -> anyhow::Result<()> { - match self { - PreAuthorizationPolicy::Ed25519 { - required_signatures, - allowed_signers, - } => { - #[allow(clippy::unnecessary_filter_map)] - let ed25519_pre_auths = - request - .pre_authorizations - .iter() - .filter_map(|pre_auth| match pre_auth { - PreAuthorization::Ed25519(pre_auth) => Some(pre_auth), - // _ => None, - }); - - let mut allowed_signers = allowed_signers.iter().cloned().collect::>(); - let mut seen_signers = HashSet::new(); + fn check_transaction(&self, request: &AuthorizeRequest) -> anyhow::Result<()> { + self.check_pre_authorizations( + &request.pre_authorizations, + ProtoTransactionPlan::from(request.plan.clone()).encode_to_vec(), + ) + } - for pre_auth in ed25519_pre_auths { - // Remove the signer from the allowed signers set, so that - // each signer can only submit one pre-authorization. - if let Some(signer) = allowed_signers.take(&pre_auth.vk) { - pre_auth.verify_plan(&request.plan)?; - seen_signers.insert(signer); - } - } + fn check_validator_definition( + &self, + request: &AuthorizeValidatorDefinitionRequest, + ) -> anyhow::Result<()> { + self.check_pre_authorizations( + &request.pre_authorizations, + ProtoValidator::from(request.validator_definition.clone()).encode_to_vec(), + ) + } - if seen_signers.len() < *required_signatures as usize { - anyhow::bail!( - "required {} pre-authorization signatures but only saw {}", - required_signatures, - seen_signers.len(), - ); - } - Ok(()) - } - } + fn check_validator_vote(&self, request: &AuthorizeValidatorVoteRequest) -> anyhow::Result<()> { + self.check_pre_authorizations( + &request.pre_authorizations, + ProtoValidatorVoteBody::from(request.validator_vote.clone()).encode_to_vec(), + ) } } diff --git a/crates/custody/src/pre_auth.rs b/crates/custody/src/pre_auth.rs index d170832305..8105d6cac9 100644 --- a/crates/custody/src/pre_auth.rs +++ b/crates/custody/src/pre_auth.rs @@ -1,5 +1,4 @@ use penumbra_proto::{custody::v1 as pb, DomainType}; -use penumbra_transaction::TransactionPlan; use serde::{Deserialize, Serialize}; /// A pre-authorization packet. This allows a custodian to delegate (partial) @@ -28,9 +27,9 @@ pub struct Ed25519 { impl Ed25519 { /// Verifies the provided `TransactionPlan`. - pub fn verify_plan(&self, plan: &TransactionPlan) -> anyhow::Result<()> { - let plan_bytes = plan.encode_to_vec(); - self.vk.verify(&self.sig, &plan_bytes).map_err(Into::into) + pub fn verify(&self, message: impl AsRef<[u8]>) -> anyhow::Result<()> { + let bytes = message.as_ref(); + self.vk.verify(&self.sig, &bytes).map_err(Into::into) } } diff --git a/crates/custody/src/request.rs b/crates/custody/src/request.rs index bc0d26e3ac..1d5398a264 100644 --- a/crates/custody/src/request.rs +++ b/crates/custody/src/request.rs @@ -1,4 +1,6 @@ +use penumbra_governance::ValidatorVoteBody; use penumbra_proto::{custody::v1 as pb, DomainType}; +use penumbra_stake::validator::Validator; use penumbra_transaction::TransactionPlan; use crate::PreAuthorization; @@ -45,3 +47,89 @@ impl From for pb::AuthorizeRequest { } } } + +/// A validator definition authorization request submitted to a custody service for approval. +#[derive(Debug, Clone)] +pub struct AuthorizeValidatorDefinitionRequest { + /// The validator definition to authorize. + pub validator_definition: Validator, + /// Optionally, pre-authorization data, if required by the custodian. + pub pre_authorizations: Vec, +} + +impl DomainType for AuthorizeValidatorDefinitionRequest { + type Proto = pb::AuthorizeValidatorDefinitionRequest; +} + +impl TryFrom for AuthorizeValidatorDefinitionRequest { + type Error = anyhow::Error; + fn try_from(value: pb::AuthorizeValidatorDefinitionRequest) -> Result { + Ok(Self { + validator_definition: value + .validator_definition + .ok_or_else(|| anyhow::anyhow!("missing validator definition"))? + .try_into()?, + pre_authorizations: value + .pre_authorizations + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + }) + } +} + +impl From for pb::AuthorizeValidatorDefinitionRequest { + fn from(value: AuthorizeValidatorDefinitionRequest) -> pb::AuthorizeValidatorDefinitionRequest { + Self { + validator_definition: Some(value.validator_definition.into()), + pre_authorizations: value + .pre_authorizations + .into_iter() + .map(Into::into) + .collect(), + } + } +} + +/// A validator vote authorization request submitted to a custody service for approval. +#[derive(Debug, Clone)] +pub struct AuthorizeValidatorVoteRequest { + /// The transaction plan to authorize. + pub validator_vote: ValidatorVoteBody, + /// Optionally, pre-authorization data, if required by the custodian. + pub pre_authorizations: Vec, +} + +impl DomainType for AuthorizeValidatorVoteRequest { + type Proto = pb::AuthorizeValidatorVoteRequest; +} + +impl TryFrom for AuthorizeValidatorVoteRequest { + type Error = anyhow::Error; + fn try_from(value: pb::AuthorizeValidatorVoteRequest) -> Result { + Ok(Self { + validator_vote: value + .validator_vote + .ok_or_else(|| anyhow::anyhow!("missing validator vote"))? + .try_into()?, + pre_authorizations: value + .pre_authorizations + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + }) + } +} + +impl From for pb::AuthorizeValidatorVoteRequest { + fn from(value: AuthorizeValidatorVoteRequest) -> pb::AuthorizeValidatorVoteRequest { + Self { + validator_vote: Some(value.validator_vote.into()), + pre_authorizations: value + .pre_authorizations + .into_iter() + .map(Into::into) + .collect(), + } + } +} diff --git a/crates/custody/src/soft_kms.rs b/crates/custody/src/soft_kms.rs index 938ee027a4..4e49b654be 100644 --- a/crates/custody/src/soft_kms.rs +++ b/crates/custody/src/soft_kms.rs @@ -1,12 +1,23 @@ //! A basic software key management system that stores keys in memory but //! presents as an asynchronous signer. -use penumbra_proto::custody::v1::{self as pb, AuthorizeResponse}; +use decaf377_rdsa::{Signature, SpendAuth}; +use penumbra_proto::{ + core::component::{ + governance::v1::ValidatorVoteBody as ProtoValidatorVoteBody, + stake::v1::Validator as ProtoValidator, + }, + custody::v1::{self as pb, AuthorizeResponse}, + Message as _, +}; use penumbra_transaction::AuthorizationData; use rand_core::OsRng; use tonic::{async_trait, Request, Response, Status}; -use crate::{policy::Policy, AuthorizeRequest}; +use crate::{ + policy::Policy, AuthorizeRequest, AuthorizeValidatorDefinitionRequest, + AuthorizeValidatorVoteRequest, +}; mod config; @@ -30,11 +41,55 @@ impl SoftKms { tracing::debug!(?request.plan); for policy in &self.config.auth_policy { - policy.check(request)?; + policy.check_transaction(request)?; } Ok(request.plan.authorize(OsRng, &self.config.spend_key)?) } + + /// Attempt to authorize the requested validator definition. + #[tracing::instrument(skip(self, request), name = "softhsm_sign_validator_definition")] + pub fn sign_validator_definition( + &self, + request: &AuthorizeValidatorDefinitionRequest, + ) -> anyhow::Result> { + tracing::debug!(?request.validator_definition); + + for policy in &self.config.auth_policy { + policy.check_validator_definition(request)?; + } + + let protobuf_serialized: ProtoValidator = request.validator_definition.clone().into(); + let validator_definition_bytes = protobuf_serialized.encode_to_vec(); + + Ok(self + .config + .spend_key + .spend_auth_key() + .sign(OsRng, &validator_definition_bytes)) + } + + /// Attempt to authorize the requested validator vote. + #[tracing::instrument(skip(self, request), name = "softhsm_sign_validator_vote")] + pub fn sign_validator_vote( + &self, + request: &AuthorizeValidatorVoteRequest, + ) -> anyhow::Result> { + tracing::debug!(?request.validator_vote); + + for policy in &self.config.auth_policy { + policy.check_validator_vote(request)?; + } + + let protobuf_serialized: ProtoValidatorVoteBody = request.validator_vote.clone().into(); + let validator_vote_bytes = protobuf_serialized.encode_to_vec(); + + Ok(self + .config + .spend_key + .spend_auth_key() + .sign(OsRng, &validator_vote_bytes)) + } } #[async_trait] @@ -59,6 +114,46 @@ impl pb::custody_service_server::CustodyService for SoftKms { Ok(Response::new(authorization_response)) } + async fn authorize_validator_definition( + &self, + request: Request, + ) -> Result, Status> { + let request = request + .into_inner() + .try_into() + .map_err(|e: anyhow::Error| Status::invalid_argument(e.to_string()))?; + + let validator_definition_auth = self + .sign_validator_definition(&request) + .map_err(|e| Status::unauthenticated(format!("{e:#}")))?; + + let authorization_response = pb::AuthorizeValidatorDefinitionResponse { + validator_definition_auth: Some(validator_definition_auth.into()), + }; + + Ok(Response::new(authorization_response)) + } + + async fn authorize_validator_vote( + &self, + request: Request, + ) -> Result, Status> { + let request = request + .into_inner() + .try_into() + .map_err(|e: anyhow::Error| Status::invalid_argument(e.to_string()))?; + + let validator_vote_auth = self + .sign_validator_vote(&request) + .map_err(|e| Status::unauthenticated(format!("{e:#}")))?; + + let authorization_response = pb::AuthorizeValidatorVoteResponse { + validator_vote_auth: Some(validator_vote_auth.into()), + }; + + Ok(Response::new(authorization_response)) + } + async fn export_full_viewing_key( &self, _request: Request, diff --git a/crates/custody/src/threshold.rs b/crates/custody/src/threshold.rs index 8bb5a60e4d..48e10d0f86 100644 --- a/crates/custody/src/threshold.rs +++ b/crates/custody/src/threshold.rs @@ -5,11 +5,12 @@ use tonic::{async_trait, Request, Response, Status}; use penumbra_keys::{keys::AddressIndex, Address, FullViewingKey}; use penumbra_proto::{custody::v1 as pb, DomainType}; -use penumbra_transaction::{AuthorizationData, TransactionPlan}; -use crate::AuthorizeRequest; +use crate::{AuthorizeRequest, AuthorizeValidatorDefinitionRequest, AuthorizeValidatorVoteRequest}; pub use self::config::Config; +use self::sign::no_signature_response; +pub use self::sign::{SigningRequest, SigningResponse}; mod config; mod dkg; @@ -39,11 +40,12 @@ where /// interface, but it can also be plugged in with more general backends. #[async_trait] pub trait Terminal { - /// Have a user confirm that they want to sign this transaction. + /// Have a user confirm that they want to sign this transaction or other data (e.g. validator + /// definition, validator vote). /// - /// In an actual terminal, this should display the transaction in a human readable + /// In an actual terminal, this should display the data to be signed in a human readable /// form, and then get feedback from the user. - async fn confirm_transaction(&self, transaction: &TransactionPlan) -> Result; + async fn confirm_request(&self, request: &SigningRequest) -> Result; /// Push an explanatory message to the terminal. /// @@ -67,7 +69,11 @@ pub trait Terminal { /// /// All this function does is produce side effects on the terminal, potentially returning /// early if the user on the other end did not want to sign the transaction. -pub async fn follow(config: &Config, terminal: &impl Terminal) -> Result<()> { +pub async fn follow( + config: Option<&Config>, + governance_config: Option<&Config>, + terminal: &impl Terminal, +) -> Result<()> { // Round 1 terminal .explain("Paste the coordinator's first message:") @@ -79,7 +85,22 @@ pub async fn follow(config: &Config, terminal: &impl Terminal) -> Result<()> { .ok_or(anyhow!("expected message from coordinator"))?; from_json(&string)? }; - if !terminal.confirm_transaction(&round1_message.plan()).await? { + // Pick the right config based on the message + let config = match round1_message.signing_request() { + SigningRequest::TransactionPlan(_) => config.ok_or(anyhow!( + "cannot threshold sign transaction using a non-threshold custody backend" + ))?, + SigningRequest::ValidatorDefinition(_) => config.ok_or(anyhow!( + "cannot threshold sign validator definition using a non-threshold custody backend" + ))?, + SigningRequest::ValidatorVote(_) => governance_config.ok_or(anyhow!( + "cannot threshold sign validator vote using a non-threshold validator governance custody backend" + ))?, + }; + if !terminal + .confirm_request(round1_message.signing_request()) + .await? + { return Ok(()); } let (round1_reply, round1_state) = sign::follower_round1(&mut OsRng, config, round1_message)?; @@ -166,7 +187,7 @@ pub async fn dkg(t: u16, n: u16, terminal: &impl Terminal) -> Result { dkg::round3(&mut OsRng, state, round2_replies) } -/// A custody backend using threshold signing. +/// A custody backend using threshold signing. /// /// This backend is initialized with a full viewing key, but only a share /// of the spend key, which is not enough to sign on its own. Instead, @@ -185,11 +206,14 @@ impl Threshold { impl Threshold { /// Try and create the necessary signatures to authorize the transaction plan. - async fn authorize(&self, request: AuthorizeRequest) -> Result { - let plan = request.plan; - + async fn authorize(&self, request: SigningRequest) -> Result { + // Some requests will have no signatures to gather, so there's no need + // to send around empty threshold signature requests. + if let Some(out) = no_signature_response(self.config.fvk(), &request)? { + return Ok(out); + } // Round 1 - let (round1_message, state1) = sign::coordinator_round1(&mut OsRng, &self.config, plan)?; + let (round1_message, state1) = sign::coordinator_round1(&mut OsRng, &self.config, request)?; self.terminal .explain("Send this message to the other signers:") .await?; @@ -197,7 +221,7 @@ impl Threshold { self.terminal .explain(&format!( "Now, gather at least {} replies from the other signers, and paste them below:", - self.config.threshold() + self.config.threshold() - 1 )) .await?; let round1_replies = { @@ -265,18 +289,83 @@ impl pb::custody_service_server::CustodySer &self, request: Request, ) -> Result, Status> { - let request = request + let request: AuthorizeRequest = request .into_inner() .try_into() .map_err(|e| Status::invalid_argument(format!("{e}")))?; - let data = self.authorize(request).await.map_err(|e| { - Status::internal(format!("Failed to process authorization request: {e}")) - })?; + let data = self + .authorize(SigningRequest::TransactionPlan(request.plan)) + .await + .map_err(|e| { + Status::internal(format!( + "Failed to process transaction authorization request: {e}" + )) + })?; + let SigningResponse::Transaction(data) = data else { + return Err(Status::internal( + "expected transaction authorization but custody service returned another kind of authorization data" + .to_string() + )); + }; Ok(Response::new(pb::AuthorizeResponse { data: Some(data.into()), })) } + async fn authorize_validator_definition( + &self, + request: Request, + ) -> Result, Status> { + let request: AuthorizeValidatorDefinitionRequest = request + .into_inner() + .try_into() + .map_err(|e| Status::invalid_argument(format!("{e}")))?; + let data = self + .authorize(SigningRequest::ValidatorDefinition( + request.validator_definition, + )) + .await + .map_err(|e| { + Status::internal(format!( + "Failed to process validator definition authorization request: {e}" + )) + })?; + let SigningResponse::ValidatorDefinition(validator_definition_auth) = data else { + return Err(Status::internal( + "expected validator definition authorization but custody service returned another kind of authorization data".to_string() + )); + }; + Ok(Response::new(pb::AuthorizeValidatorDefinitionResponse { + validator_definition_auth: Some(validator_definition_auth.into()), + })) + } + + async fn authorize_validator_vote( + &self, + request: Request, + ) -> Result, Status> { + let request: AuthorizeValidatorVoteRequest = request + .into_inner() + .try_into() + .map_err(|e| Status::invalid_argument(format!("{e}")))?; + let data = self + .authorize(SigningRequest::ValidatorVote(request.validator_vote)) + .await + .map_err(|e| { + Status::internal(format!( + "Failed to process validator vote authorization request: {e}" + )) + })?; + let SigningResponse::ValidatorVote(validator_vote_auth) = data else { + return Err(Status::internal( + "expected validator vote authorization but custody service returned another kind of authorization data".to_string() + )); + }; + Ok(Response::new(pb::AuthorizeValidatorVoteResponse { + validator_vote_auth: Some(validator_vote_auth.into()), + })) + } + async fn export_full_viewing_key( &self, _request: Request, @@ -308,6 +397,8 @@ impl pb::custody_service_server::CustodySer mod test { use std::collections::HashMap; + use penumbra_transaction::TransactionPlan; + use tokio::sync; use super::*; @@ -319,7 +410,7 @@ mod test { #[async_trait] impl Terminal for FollowerTerminal { - async fn confirm_transaction(&self, _transaction: &TransactionPlan) -> Result { + async fn confirm_request(&self, _request: &SigningRequest) -> Result { Ok(true) } @@ -357,7 +448,7 @@ mod test { #[async_trait] impl Terminal for CoordinatorTerminal { - async fn confirm_transaction(&self, _transaction: &TransactionPlan) -> Result { + async fn confirm_request(&self, _request: &SigningRequest) -> Result { Ok(true) } @@ -567,31 +658,31 @@ mod test { .into_iter() .zip(follower_terminals.into_iter()) { - tokio::spawn(async move { follow(&config, &terminal).await }); + tokio::spawn(async move { follow(Some(&config), Some(&config), &terminal).await }); } let plan = serde_json::from_str::(TEST_PLAN)?; let fvk = coordinator_config.fvk().clone(); let authorization_data = Threshold::new(coordinator_config, coordinator_terminal) - .authorize(AuthorizeRequest { - plan: plan.clone(), - pre_authorizations: Vec::new(), - }) + .authorize(SigningRequest::TransactionPlan(plan.clone())) .await?; + let tx_authorization_data = match authorization_data { + SigningResponse::Transaction(tx) => tx, + _ => panic!("expected transaction authorization data"), + }; assert_eq!( plan.effect_hash(&fvk)?, - authorization_data + tx_authorization_data .effect_hash .expect("effect hash not present") ); // The transaction plan only has spends for (randomizer, sig) in plan .spend_plans() - .into_iter() .map(|x| x.randomizer) - .zip(authorization_data.spend_auths) + .zip(tx_authorization_data.spend_auths) { fvk.spend_verification_key().randomize(&randomizer).verify( - authorization_data + tx_authorization_data .effect_hash .expect("effect hash not present") .as_bytes(), diff --git a/crates/custody/src/threshold/sign.rs b/crates/custody/src/threshold/sign.rs index 60c21e1648..b23c6024e2 100644 --- a/crates/custody/src/threshold/sign.rs +++ b/crates/custody/src/threshold/sign.rs @@ -5,11 +5,18 @@ use std::{ use anyhow::{anyhow, Result}; use ed25519_consensus::{Signature, SigningKey, VerificationKey}; +use penumbra_keys::FullViewingKey; use rand_core::CryptoRngCore; use decaf377_frost as frost; use frost::round1::SigningCommitments; +use penumbra_governance::ValidatorVoteBody; +use penumbra_proto::core::component::{ + governance::v1::ValidatorVoteBody as ProtoValidatorVoteBody, + stake::v1::Validator as ProtoValidator, +}; use penumbra_proto::{penumbra::custody::threshold::v1 as pb, DomainType, Message}; +use penumbra_stake::validator::Validator; use penumbra_transaction::{AuthorizationData, TransactionPlan}; use penumbra_txhash::EffectHash; @@ -20,22 +27,59 @@ use super::config::Config; /// This is nominally "round 1", even though it's the only message the coordinator ever sends. #[derive(Debug, Clone)] pub struct CoordinatorRound1 { - plan: TransactionPlan, + request: SigningRequest, +} + +#[derive(Debug, Clone)] +pub enum SigningRequest { + TransactionPlan(TransactionPlan), + ValidatorDefinition(Validator), + ValidatorVote(ValidatorVoteBody), +} + +/// Authorization data returned in response to some signing request, which may be a request to +/// authorize a transaction, a validator definition, or a validator vote. +#[derive(Clone, Debug)] +pub enum SigningResponse { + /// Authorization data for a transaction. + Transaction(AuthorizationData), + /// Authorization signature for a validator definition. + ValidatorDefinition(decaf377_rdsa::Signature), + /// Authorization signature for a validator vote. + ValidatorVote(decaf377_rdsa::Signature), +} + +impl From for SigningResponse { + fn from(msg: AuthorizationData) -> Self { + Self::Transaction(msg) + } } impl CoordinatorRound1 { /// View the transaction plan associated with the first message. /// /// We need this method to be able to prompt users correctly. - pub fn plan(&self) -> &TransactionPlan { - &self.plan + pub fn signing_request(&self) -> &SigningRequest { + &self.request } } impl From for pb::CoordinatorRound1 { fn from(value: CoordinatorRound1) -> Self { - Self { - plan: Some(value.plan.into()), + match value.request { + SigningRequest::TransactionPlan(plan) => Self { + request: Some(pb::coordinator_round1::Request::Plan(plan.into())), + }, + SigningRequest::ValidatorDefinition(validator) => Self { + request: Some(pb::coordinator_round1::Request::ValidatorDefinition( + ProtoValidator::from(validator).into(), + )), + }, + SigningRequest::ValidatorVote(vote) => Self { + request: Some(pb::coordinator_round1::Request::ValidatorVote( + ProtoValidatorVoteBody::from(vote).into(), + )), + }, } } } @@ -44,9 +88,20 @@ impl TryFrom for CoordinatorRound1 { type Error = anyhow::Error; fn try_from(value: pb::CoordinatorRound1) -> Result { - Ok(Self { - plan: value.plan.ok_or(anyhow!("missing plan"))?.try_into()?, - }) + match value + .request + .ok_or_else(|| anyhow::anyhow!("missing request"))? + { + pb::coordinator_round1::Request::Plan(plan) => Ok(Self { + request: SigningRequest::TransactionPlan(plan.try_into()?), + }), + pb::coordinator_round1::Request::ValidatorDefinition(def) => Ok(Self { + request: SigningRequest::ValidatorDefinition(def.try_into()?), + }), + pb::coordinator_round1::Request::ValidatorVote(vote) => Ok(Self { + request: SigningRequest::ValidatorVote(vote.try_into()?), + }), + } } } @@ -288,37 +343,95 @@ impl DomainType for FollowerRound2 { /// Calculate the number of required signatures for a plan. /// /// A plan can require more than one signature, hence the need for this method. -fn required_signatures(plan: &TransactionPlan) -> usize { - plan.spend_plans().count() + plan.delegator_vote_plans().count() +fn required_signatures(request: &SigningRequest) -> usize { + match request { + SigningRequest::TransactionPlan(plan) => { + plan.spend_plans().count() + plan.delegator_vote_plans().count() + } + SigningRequest::ValidatorDefinition(_) => 1, + SigningRequest::ValidatorVote(_) => 1, + } +} + +/// Create a trivial signing response if no signatures are needed. +pub fn no_signature_response( + fvk: &FullViewingKey, + request: &SigningRequest, +) -> Result> { + match request { + SigningRequest::TransactionPlan(plan) if required_signatures(request) <= 0 => { + Ok(Some(SigningResponse::Transaction(AuthorizationData { + effect_hash: Some(plan.effect_hash(fvk)?), + spend_auths: Vec::new(), + delegator_vote_auths: Vec::new(), + }))) + } + _ => Ok(None), + } } pub struct CoordinatorState1 { - plan: TransactionPlan, + request: SigningRequest, my_round1_reply: FollowerRound1, my_round1_state: FollowerState, } pub struct CoordinatorState2 { - plan: TransactionPlan, + request: SigningRequest, my_round2_reply: FollowerRound2, - effect_hash: EffectHash, + to_be_signed: ToBeSigned, signing_packages: Vec, } +enum ToBeSigned { + EffectHash(EffectHash), + ValidatorDefinitionBytes(Vec), + ValidatorVoteBytes(Vec), +} + +impl SigningRequest { + fn to_be_signed(&self, config: &Config) -> Result { + let out = match self { + SigningRequest::TransactionPlan(plan) => { + ToBeSigned::EffectHash(plan.effect_hash(config.fvk())?) + } + SigningRequest::ValidatorDefinition(validator) => ToBeSigned::ValidatorDefinitionBytes( + ProtoValidator::from(validator.clone()).encode_to_vec(), + ), + SigningRequest::ValidatorVote(vote) => ToBeSigned::ValidatorVoteBytes( + ProtoValidatorVoteBody::from(vote.clone()).encode_to_vec(), + ), + }; + Ok(out) + } +} + +impl AsRef<[u8]> for ToBeSigned { + fn as_ref(&self) -> &[u8] { + match self { + ToBeSigned::EffectHash(x) => x.as_ref(), + ToBeSigned::ValidatorDefinitionBytes(x) => x.as_slice(), + ToBeSigned::ValidatorVoteBytes(x) => x.as_slice(), + } + } +} + pub struct FollowerState { - plan: TransactionPlan, + request: SigningRequest, nonces: Vec, } pub fn coordinator_round1( rng: &mut impl CryptoRngCore, config: &Config, - plan: TransactionPlan, + request: SigningRequest, ) -> Result<(CoordinatorRound1, CoordinatorState1)> { - let message = CoordinatorRound1 { plan: plan.clone() }; + let message = CoordinatorRound1 { + request: request.clone(), + }; let (my_round1_reply, my_round1_state) = follower_round1(rng, config, message.clone())?; let state = CoordinatorState1 { - plan, + request, my_round1_reply, my_round1_state, }; @@ -330,7 +443,7 @@ pub fn coordinator_round2( state: CoordinatorState1, follower_messages: &[FollowerRound1], ) -> Result<(CoordinatorRound2, CoordinatorState2)> { - let mut all_commitments = vec![BTreeMap::new(); required_signatures(&state.plan)]; + let mut all_commitments = vec![BTreeMap::new(); required_signatures(&state.request)]; for message in follower_messages .iter() .cloned() @@ -349,18 +462,20 @@ pub fn coordinator_round2( let reply = CoordinatorRound2 { all_commitments }; let my_round2_reply = follower_round2(config, state.my_round1_state, reply.clone())?; - let effect_hash = state.plan.effect_hash(config.fvk())?; + + let to_be_signed = state.request.to_be_signed(&config)?; + let signing_packages = { reply .all_commitments .iter() - .map(|tree| frost::SigningPackage::new(tree.clone(), effect_hash.as_ref())) + .map(|tree| frost::SigningPackage::new(tree.clone(), to_be_signed.as_ref())) .collect() }; let state = CoordinatorState2 { - plan: state.plan, + request: state.request, my_round2_reply, - effect_hash, + to_be_signed, signing_packages, }; Ok((reply, state)) @@ -370,9 +485,9 @@ pub fn coordinator_round3( config: &Config, state: CoordinatorState2, follower_messages: &[FollowerRound2], -) -> Result { +) -> Result { let mut share_maps: Vec> = - vec![HashMap::new(); required_signatures(&state.plan)]; + vec![HashMap::new(); required_signatures(&state.request)]; for message in follower_messages .iter() .cloned() @@ -387,28 +502,63 @@ pub fn coordinator_round3( map_i.insert(identifier, share_i); } } - let mut spend_auths = state - .plan - .spend_plans() - .map(|x| x.randomizer) - .chain(state.plan.delegator_vote_plans().map(|x| x.randomizer)) - .zip(share_maps.iter()) - .zip(state.signing_packages.iter()) - .map(|((randomizer, share_map), signing_package)| { - frost::aggregate_randomized( - signing_package, - &share_map, + + match state.request { + SigningRequest::TransactionPlan(plan) => { + let mut spend_auths = plan + .spend_plans() + .map(|x| x.randomizer) + .chain(plan.delegator_vote_plans().map(|x| x.randomizer)) + .zip(share_maps.iter()) + .zip(state.signing_packages.iter()) + .map(|((randomizer, share_map), signing_package)| { + frost::aggregate_randomized( + signing_package, + &share_map, + &config.public_key_package(), + randomizer, + ) + }) + .collect::, _>>()?; + let delegator_vote_auths = spend_auths.split_off(plan.spend_plans().count()); + Ok(SigningResponse::Transaction(AuthorizationData { + effect_hash: { + let ToBeSigned::EffectHash(effect_hash) = state.to_be_signed else { + unreachable!("transaction plan request has non-effect-hash to be signed"); + }; + Some(effect_hash) + }, + spend_auths, + delegator_vote_auths, + })) + } + SigningRequest::ValidatorDefinition(_) => { + let validator_definition_auth = share_maps + .get(0) + .ok_or_else(|| anyhow!("missing signature for validator definition"))?; + Ok(SigningResponse::ValidatorDefinition(frost::aggregate( + &state + .signing_packages + .get(0) + .expect("same number of signing packages as signatures"), + &validator_definition_auth, &config.public_key_package(), - randomizer, - ) - }) - .collect::, _>>()?; - let delegator_vote_auths = spend_auths.split_off(state.plan.spend_plans().count()); - Ok(AuthorizationData { - effect_hash: Some(state.effect_hash), - spend_auths, - delegator_vote_auths, - }) + )?)) + } + SigningRequest::ValidatorVote(_) => { + let validator_vote_auth = share_maps + .get(0) + .ok_or_else(|| anyhow!("missing signature for validator vote"))?; + Ok(SigningResponse::ValidatorVote(frost::aggregate( + &state + .signing_packages + .get(0) + .expect("same number of signing packages as signatures"), + &validator_vote_auth, + &config.public_key_package(), + )?)) + } + } } pub fn follower_round1( @@ -416,13 +566,13 @@ pub fn follower_round1( config: &Config, coordinator: CoordinatorRound1, ) -> Result<(FollowerRound1, FollowerState)> { - let required = required_signatures(&coordinator.plan); + let required = required_signatures(&coordinator.request); let (nonces, commitments) = (0..required) .map(|_| frost::round1::commit(&config.key_package().secret_share(), rng)) .unzip(); let reply = FollowerRound1::make(config.signing_key(), commitments); let state = FollowerState { - plan: coordinator.plan, + request: coordinator.request, nonces, }; Ok((reply, state)) @@ -433,26 +583,39 @@ pub fn follower_round2( state: FollowerState, coordinator: CoordinatorRound2, ) -> Result { - let effect_hash = state.plan.effect_hash(config.fvk())?; + let to_be_signed = state.request.to_be_signed(config)?; let signing_packages = coordinator .all_commitments .into_iter() - .map(|tree| frost::SigningPackage::new(tree, effect_hash.as_ref())); - let shares = state - .plan - .spend_plans() - .map(|x| x.randomizer) - .chain(state.plan.delegator_vote_plans().map(|x| x.randomizer)) - .zip(signing_packages) - .zip(state.nonces.into_iter()) - .map(|((randomizer, signing_package), signer_nonces)| { - frost::round2::sign_randomized( - &signing_package, - &signer_nonces, - &config.key_package(), - randomizer, - ) - }) - .collect::>()?; - Ok(FollowerRound2::make(config.signing_key(), shares)) + .map(|tree| frost::SigningPackage::new(tree, to_be_signed.as_ref())); + + match state.request { + SigningRequest::TransactionPlan(plan) => { + let shares = plan + .spend_plans() + .map(|x| x.randomizer) + .chain(plan.delegator_vote_plans().map(|x| x.randomizer)) + .zip(signing_packages) + .zip(state.nonces.into_iter()) + .map(|((randomizer, signing_package), signer_nonces)| { + frost::round2::sign_randomized( + &signing_package, + &signer_nonces, + &config.key_package(), + randomizer, + ) + }) + .collect::>()?; + Ok(FollowerRound2::make(config.signing_key(), shares)) + } + SigningRequest::ValidatorDefinition(_) | SigningRequest::ValidatorVote(_) => { + let shares = signing_packages + .zip(state.nonces.into_iter()) + .map(|(signing_package, signer_nonces)| { + frost::round2::sign(&signing_package, &signer_nonces, &config.key_package()) + }) + .collect::>()?; + Ok(FollowerRound2::make(config.signing_key(), shares)) + } + } } diff --git a/crates/proto/src/gen/penumbra.custody.threshold.v1.rs b/crates/proto/src/gen/penumbra.custody.threshold.v1.rs index 6e104f811d..2bf788a6b6 100644 --- a/crates/proto/src/gen/penumbra.custody.threshold.v1.rs +++ b/crates/proto/src/gen/penumbra.custody.threshold.v1.rs @@ -32,11 +32,28 @@ impl ::prost::Name for Signature { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct CoordinatorRound1 { - /// The plan that the coordinator would like the followers to sign. - #[prost(message, optional, tag = "1")] - pub plan: ::core::option::Option< - super::super::super::core::transaction::v1::TransactionPlan, - >, + #[prost(oneof = "coordinator_round1::Request", tags = "1, 2, 3")] + pub request: ::core::option::Option, +} +/// Nested message and enum types in `CoordinatorRound1`. +pub mod coordinator_round1 { + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Request { + /// The plan that the coordinator would like the followers to sign. + #[prost(message, tag = "1")] + Plan(super::super::super::super::core::transaction::v1::TransactionPlan), + /// The validator definition the coordinator would like the followers to sign. + #[prost(message, tag = "2")] + ValidatorDefinition( + super::super::super::super::core::component::stake::v1::Validator, + ), + /// The validator vote the coordinator would like the followers to sign. + #[prost(message, tag = "3")] + ValidatorVote( + super::super::super::super::core::component::governance::v1::ValidatorVoteBody, + ), + } } impl ::prost::Name for CoordinatorRound1 { const NAME: &'static str = "CoordinatorRound1"; diff --git a/crates/proto/src/gen/penumbra.custody.threshold.v1.serde.rs b/crates/proto/src/gen/penumbra.custody.threshold.v1.serde.rs index 1daf0f26cf..86ecbc1f91 100644 --- a/crates/proto/src/gen/penumbra.custody.threshold.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.custody.threshold.v1.serde.rs @@ -6,12 +6,22 @@ impl serde::Serialize for CoordinatorRound1 { { use serde::ser::SerializeStruct; let mut len = 0; - if self.plan.is_some() { + if self.request.is_some() { len += 1; } let mut struct_ser = serializer.serialize_struct("penumbra.custody.threshold.v1.CoordinatorRound1", len)?; - if let Some(v) = self.plan.as_ref() { - struct_ser.serialize_field("plan", v)?; + if let Some(v) = self.request.as_ref() { + match v { + coordinator_round1::Request::Plan(v) => { + struct_ser.serialize_field("plan", v)?; + } + coordinator_round1::Request::ValidatorDefinition(v) => { + struct_ser.serialize_field("validatorDefinition", v)?; + } + coordinator_round1::Request::ValidatorVote(v) => { + struct_ser.serialize_field("validatorVote", v)?; + } + } } struct_ser.end() } @@ -24,11 +34,17 @@ impl<'de> serde::Deserialize<'de> for CoordinatorRound1 { { const FIELDS: &[&str] = &[ "plan", + "validator_definition", + "validatorDefinition", + "validator_vote", + "validatorVote", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { Plan, + ValidatorDefinition, + ValidatorVote, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -52,6 +68,8 @@ impl<'de> serde::Deserialize<'de> for CoordinatorRound1 { { match value { "plan" => Ok(GeneratedField::Plan), + "validatorDefinition" | "validator_definition" => Ok(GeneratedField::ValidatorDefinition), + "validatorVote" | "validator_vote" => Ok(GeneratedField::ValidatorVote), _ => Ok(GeneratedField::__SkipField__), } } @@ -71,14 +89,29 @@ impl<'de> serde::Deserialize<'de> for CoordinatorRound1 { where V: serde::de::MapAccess<'de>, { - let mut plan__ = None; + let mut request__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Plan => { - if plan__.is_some() { + if request__.is_some() { return Err(serde::de::Error::duplicate_field("plan")); } - plan__ = map_.next_value()?; + request__ = map_.next_value::<::std::option::Option<_>>()?.map(coordinator_round1::Request::Plan) +; + } + GeneratedField::ValidatorDefinition => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("validatorDefinition")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(coordinator_round1::Request::ValidatorDefinition) +; + } + GeneratedField::ValidatorVote => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("validatorVote")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(coordinator_round1::Request::ValidatorVote) +; } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; @@ -86,7 +119,7 @@ impl<'de> serde::Deserialize<'de> for CoordinatorRound1 { } } Ok(CoordinatorRound1 { - plan: plan__, + request: request__, }) } } diff --git a/crates/proto/src/gen/penumbra.custody.v1.rs b/crates/proto/src/gen/penumbra.custody.v1.rs index 22c7d945de..82fe5c426b 100644 --- a/crates/proto/src/gen/penumbra.custody.v1.rs +++ b/crates/proto/src/gen/penumbra.custody.v1.rs @@ -37,6 +37,86 @@ impl ::prost::Name for AuthorizeResponse { ::prost::alloc::format!("penumbra.custody.v1.{}", Self::NAME) } } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AuthorizeValidatorDefinitionRequest { + /// The validator definition to authorize. + #[prost(message, optional, tag = "1")] + pub validator_definition: ::core::option::Option< + super::super::core::component::stake::v1::Validator, + >, + /// Optionally, pre-authorization data, if required by the custodian. + /// + /// Pre-authorization data is backend-specific, and backends are free to ignore it. + /// + /// Multiple `PreAuthorization` packets can be included in a single request, + /// to support multi-party pre-authorizations. + #[prost(message, repeated, tag = "3")] + pub pre_authorizations: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for AuthorizeValidatorDefinitionRequest { + const NAME: &'static str = "AuthorizeValidatorDefinitionRequest"; + const PACKAGE: &'static str = "penumbra.custody.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.custody.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AuthorizeValidatorDefinitionResponse { + /// The authorization signature for the validator definition. + #[prost(message, optional, tag = "1")] + pub validator_definition_auth: ::core::option::Option< + super::super::crypto::decaf377_rdsa::v1::SpendAuthSignature, + >, +} +impl ::prost::Name for AuthorizeValidatorDefinitionResponse { + const NAME: &'static str = "AuthorizeValidatorDefinitionResponse"; + const PACKAGE: &'static str = "penumbra.custody.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.custody.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AuthorizeValidatorVoteRequest { + /// The validator vote to authorize. + #[prost(message, optional, tag = "1")] + pub validator_vote: ::core::option::Option< + super::super::core::component::governance::v1::ValidatorVoteBody, + >, + /// Optionally, pre-authorization data, if required by the custodian. + /// + /// Pre-authorization data is backend-specific, and backends are free to ignore it. + /// + /// Multiple `PreAuthorization` packets can be included in a single request, + /// to support multi-party pre-authorizations. + #[prost(message, repeated, tag = "3")] + pub pre_authorizations: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for AuthorizeValidatorVoteRequest { + const NAME: &'static str = "AuthorizeValidatorVoteRequest"; + const PACKAGE: &'static str = "penumbra.custody.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.custody.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AuthorizeValidatorVoteResponse { + /// The authorization signature for the validator vote. + #[prost(message, optional, tag = "1")] + pub validator_vote_auth: ::core::option::Option< + super::super::crypto::decaf377_rdsa::v1::SpendAuthSignature, + >, +} +impl ::prost::Name for AuthorizeValidatorVoteResponse { + const NAME: &'static str = "AuthorizeValidatorVoteResponse"; + const PACKAGE: &'static str = "penumbra.custody.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.custody.v1.{}", Self::NAME) + } +} /// A pre-authorization packet. This allows a custodian to delegate (partial) /// signing authority to other authorization mechanisms. Details of how a /// custodian manages those keys are out-of-scope for the custody protocol and @@ -263,6 +343,68 @@ pub mod custody_service_client { ); self.inner.unary(req, path, codec).await } + /// Requests authorization of the given validator definition update. + pub async fn authorize_validator_definition( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/penumbra.custody.v1.CustodyService/AuthorizeValidatorDefinition", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "penumbra.custody.v1.CustodyService", + "AuthorizeValidatorDefinition", + ), + ); + self.inner.unary(req, path, codec).await + } + /// Requests authorization of the given validator vote. + pub async fn authorize_validator_vote( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/penumbra.custody.v1.CustodyService/AuthorizeValidatorVote", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "penumbra.custody.v1.CustodyService", + "AuthorizeValidatorVote", + ), + ); + self.inner.unary(req, path, codec).await + } /// Requests the full viewing key from the custodian. /// /// Custody backends should decide whether to honor this request, and how to @@ -351,6 +493,22 @@ pub mod custody_service_server { tonic::Response, tonic::Status, >; + /// Requests authorization of the given validator definition update. + async fn authorize_validator_definition( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + /// Requests authorization of the given validator vote. + async fn authorize_validator_vote( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; /// Requests the full viewing key from the custodian. /// /// Custody backends should decide whether to honor this request, and how to @@ -512,6 +670,111 @@ pub mod custody_service_server { }; Box::pin(fut) } + "/penumbra.custody.v1.CustodyService/AuthorizeValidatorDefinition" => { + #[allow(non_camel_case_types)] + struct AuthorizeValidatorDefinitionSvc( + pub Arc, + ); + impl< + T: CustodyService, + > tonic::server::UnaryService< + super::AuthorizeValidatorDefinitionRequest, + > for AuthorizeValidatorDefinitionSvc { + type Response = super::AuthorizeValidatorDefinitionResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::AuthorizeValidatorDefinitionRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::authorize_validator_definition( + &inner, + request, + ) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = AuthorizeValidatorDefinitionSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/penumbra.custody.v1.CustodyService/AuthorizeValidatorVote" => { + #[allow(non_camel_case_types)] + struct AuthorizeValidatorVoteSvc(pub Arc); + impl< + T: CustodyService, + > tonic::server::UnaryService + for AuthorizeValidatorVoteSvc { + type Response = super::AuthorizeValidatorVoteResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::authorize_validator_vote( + &inner, + request, + ) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = AuthorizeValidatorVoteSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } "/penumbra.custody.v1.CustodyService/ExportFullViewingKey" => { #[allow(non_camel_case_types)] struct ExportFullViewingKeySvc(pub Arc); diff --git a/crates/proto/src/gen/penumbra.custody.v1.serde.rs b/crates/proto/src/gen/penumbra.custody.v1.serde.rs index b5ab2c644b..2b510aa434 100644 --- a/crates/proto/src/gen/penumbra.custody.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.custody.v1.serde.rs @@ -206,6 +206,426 @@ impl<'de> serde::Deserialize<'de> for AuthorizeResponse { deserializer.deserialize_struct("penumbra.custody.v1.AuthorizeResponse", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for AuthorizeValidatorDefinitionRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.validator_definition.is_some() { + len += 1; + } + if !self.pre_authorizations.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.custody.v1.AuthorizeValidatorDefinitionRequest", len)?; + if let Some(v) = self.validator_definition.as_ref() { + struct_ser.serialize_field("validatorDefinition", v)?; + } + if !self.pre_authorizations.is_empty() { + struct_ser.serialize_field("preAuthorizations", &self.pre_authorizations)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for AuthorizeValidatorDefinitionRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "validator_definition", + "validatorDefinition", + "pre_authorizations", + "preAuthorizations", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + ValidatorDefinition, + PreAuthorizations, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "validatorDefinition" | "validator_definition" => Ok(GeneratedField::ValidatorDefinition), + "preAuthorizations" | "pre_authorizations" => Ok(GeneratedField::PreAuthorizations), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = AuthorizeValidatorDefinitionRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.custody.v1.AuthorizeValidatorDefinitionRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut validator_definition__ = None; + let mut pre_authorizations__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::ValidatorDefinition => { + if validator_definition__.is_some() { + return Err(serde::de::Error::duplicate_field("validatorDefinition")); + } + validator_definition__ = map_.next_value()?; + } + GeneratedField::PreAuthorizations => { + if pre_authorizations__.is_some() { + return Err(serde::de::Error::duplicate_field("preAuthorizations")); + } + pre_authorizations__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(AuthorizeValidatorDefinitionRequest { + validator_definition: validator_definition__, + pre_authorizations: pre_authorizations__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.custody.v1.AuthorizeValidatorDefinitionRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for AuthorizeValidatorDefinitionResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.validator_definition_auth.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.custody.v1.AuthorizeValidatorDefinitionResponse", len)?; + if let Some(v) = self.validator_definition_auth.as_ref() { + struct_ser.serialize_field("validatorDefinitionAuth", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for AuthorizeValidatorDefinitionResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "validator_definition_auth", + "validatorDefinitionAuth", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + ValidatorDefinitionAuth, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "validatorDefinitionAuth" | "validator_definition_auth" => Ok(GeneratedField::ValidatorDefinitionAuth), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = AuthorizeValidatorDefinitionResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.custody.v1.AuthorizeValidatorDefinitionResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut validator_definition_auth__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::ValidatorDefinitionAuth => { + if validator_definition_auth__.is_some() { + return Err(serde::de::Error::duplicate_field("validatorDefinitionAuth")); + } + validator_definition_auth__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(AuthorizeValidatorDefinitionResponse { + validator_definition_auth: validator_definition_auth__, + }) + } + } + deserializer.deserialize_struct("penumbra.custody.v1.AuthorizeValidatorDefinitionResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for AuthorizeValidatorVoteRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.validator_vote.is_some() { + len += 1; + } + if !self.pre_authorizations.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.custody.v1.AuthorizeValidatorVoteRequest", len)?; + if let Some(v) = self.validator_vote.as_ref() { + struct_ser.serialize_field("validatorVote", v)?; + } + if !self.pre_authorizations.is_empty() { + struct_ser.serialize_field("preAuthorizations", &self.pre_authorizations)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for AuthorizeValidatorVoteRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "validator_vote", + "validatorVote", + "pre_authorizations", + "preAuthorizations", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + ValidatorVote, + PreAuthorizations, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "validatorVote" | "validator_vote" => Ok(GeneratedField::ValidatorVote), + "preAuthorizations" | "pre_authorizations" => Ok(GeneratedField::PreAuthorizations), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = AuthorizeValidatorVoteRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.custody.v1.AuthorizeValidatorVoteRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut validator_vote__ = None; + let mut pre_authorizations__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::ValidatorVote => { + if validator_vote__.is_some() { + return Err(serde::de::Error::duplicate_field("validatorVote")); + } + validator_vote__ = map_.next_value()?; + } + GeneratedField::PreAuthorizations => { + if pre_authorizations__.is_some() { + return Err(serde::de::Error::duplicate_field("preAuthorizations")); + } + pre_authorizations__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(AuthorizeValidatorVoteRequest { + validator_vote: validator_vote__, + pre_authorizations: pre_authorizations__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.custody.v1.AuthorizeValidatorVoteRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for AuthorizeValidatorVoteResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.validator_vote_auth.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.custody.v1.AuthorizeValidatorVoteResponse", len)?; + if let Some(v) = self.validator_vote_auth.as_ref() { + struct_ser.serialize_field("validatorVoteAuth", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for AuthorizeValidatorVoteResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "validator_vote_auth", + "validatorVoteAuth", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + ValidatorVoteAuth, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "validatorVoteAuth" | "validator_vote_auth" => Ok(GeneratedField::ValidatorVoteAuth), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = AuthorizeValidatorVoteResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.custody.v1.AuthorizeValidatorVoteResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut validator_vote_auth__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::ValidatorVoteAuth => { + if validator_vote_auth__.is_some() { + return Err(serde::de::Error::duplicate_field("validatorVoteAuth")); + } + validator_vote_auth__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(AuthorizeValidatorVoteResponse { + validator_vote_auth: validator_vote_auth__, + }) + } + } + deserializer.deserialize_struct("penumbra.custody.v1.AuthorizeValidatorVoteResponse", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for ConfirmAddressRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 4657e8905a4e08c32fd1486e6e53667eff6d5c50..a631b5d5d476d7e4523fb183a801603fee5ad029 100644 GIT binary patch delta 31264 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 Date: Tue, 26 Mar 2024 21:51:10 -0400 Subject: [PATCH 030/168] =?UTF-8?q?feat(transaction):=20=F0=9F=93=A9=20`Ac?= =?UTF-8?q?tionPlan:=20From`=20(#4115)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cherry-picked from #4110. --- crates/core/transaction/src/plan/action.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/core/transaction/src/plan/action.rs b/crates/core/transaction/src/plan/action.rs index 32338e5ca6..370a54f5fa 100644 --- a/crates/core/transaction/src/plan/action.rs +++ b/crates/core/transaction/src/plan/action.rs @@ -280,6 +280,12 @@ impl From for ActionPlan { } } +impl From for ActionPlan { + fn from(inner: UndelegateClaimPlan) -> ActionPlan { + ActionPlan::UndelegateClaim(inner) + } +} + impl From for ActionPlan { fn from(inner: penumbra_stake::validator::Definition) -> ActionPlan { ActionPlan::ValidatorDefinition(inner) From 0505885eb9b22a6d4732d9e9151cc8d3834d49e6 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Wed, 27 Mar 2024 08:20:00 -0400 Subject: [PATCH 031/168] fee: gate `penumbra-proto/rpc` on component feature (#4114) After deploying testnet 70, we discovered that an OS specific crate popped up in the dependency tree of the `wasm` crate (used by the web extension). We typically use the `component` feature to prevent this from happening (e.g. mio relies on a variety of platform specific syscalls that simply don't exist in a wasm environment) but one slipped through. --- crates/core/component/fee/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/core/component/fee/Cargo.toml b/crates/core/component/fee/Cargo.toml index 94dcdd171b..4e3c5d7cc1 100644 --- a/crates/core/component/fee/Cargo.toml +++ b/crates/core/component/fee/Cargo.toml @@ -9,6 +9,7 @@ component = [ "cnidarium", "penumbra-proto/cnidarium", "tonic", + "penumbra-proto/rpc" ] default = ["std", "component"] std = ["ark-ff/std"] @@ -27,7 +28,7 @@ decaf377-rdsa = {workspace = true} metrics = {workspace = true} penumbra-asset = {workspace = true, default-features = false} penumbra-num = {workspace = true, default-features = false} -penumbra-proto = {workspace = true, features = ["rpc"], default-features = false} +penumbra-proto = {workspace = true, default-features = false} rand = {workspace = true} rand_core = {workspace = true, features = ["getrandom"]} serde = {workspace = true, features = ["derive"]} From d17c4bc93f4e831344c57de52343a2ca0e09f1e4 Mon Sep 17 00:00:00 2001 From: Ava Howell Date: Wed, 27 Mar 2024 05:20:36 -0700 Subject: [PATCH 032/168] pcli: fix ics20 withdrawal timeout rounding (#4111) The rounding math was wrong previously (it wasn't actually rounding) --- crates/bin/pcli/src/command/tx.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bin/pcli/src/command/tx.rs b/crates/bin/pcli/src/command/tx.rs index 8e4d1356b2..2803747423 100644 --- a/crates/bin/pcli/src/command/tx.rs +++ b/crates/bin/pcli/src/command/tx.rs @@ -1064,7 +1064,7 @@ impl TxCmd { } // round to the nearest 10 minutes - timeout_timestamp += timeout_timestamp % 600_000_000_000; + timeout_timestamp += 600_000_000_000 - (timeout_timestamp % 600_000_000_000); fn parse_denom_and_amount(value_str: &str) -> anyhow::Result<(Amount, Metadata)> { let denom_re = Regex::new(r"^([0-9.]+)(.+)$").context("denom regex invalid")?; From b2d00750fb7cc2d1cd44590bac0594abca5bb134 Mon Sep 17 00:00:00 2001 From: Conor Schaefer Date: Wed, 27 Mar 2024 08:21:14 -0700 Subject: [PATCH 033/168] chore: release version 0.70.1 --- Cargo.lock | 88 +++++++++++++++--------------- Cargo.toml | 2 +- docs/guide/src/penumbra_version.md | 2 +- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a93223ca59..d49c1818bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1378,7 +1378,7 @@ dependencies = [ [[package]] name = "cnidarium" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "async-trait", @@ -1413,7 +1413,7 @@ dependencies = [ [[package]] name = "cnidarium-component" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "async-trait", @@ -1892,7 +1892,7 @@ dependencies = [ [[package]] name = "decaf377-fmd" -version = "0.70.0" +version = "0.70.1" dependencies = [ "ark-ff", "ark-serialize", @@ -1907,7 +1907,7 @@ dependencies = [ [[package]] name = "decaf377-frost" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ff", @@ -1922,7 +1922,7 @@ dependencies = [ [[package]] name = "decaf377-ka" -version = "0.70.0" +version = "0.70.1" dependencies = [ "ark-ff", "decaf377 0.5.0", @@ -4397,7 +4397,7 @@ dependencies = [ [[package]] name = "pcli" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ff", @@ -4475,7 +4475,7 @@ dependencies = [ [[package]] name = "pclientd" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "assert_cmd", @@ -4527,7 +4527,7 @@ dependencies = [ [[package]] name = "pd" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ff", @@ -4666,7 +4666,7 @@ dependencies = [ [[package]] name = "penumbra-app" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ff", @@ -4746,7 +4746,7 @@ dependencies = [ [[package]] name = "penumbra-asset" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ff", @@ -4785,7 +4785,7 @@ dependencies = [ [[package]] name = "penumbra-auto-https" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "axum-server", @@ -4797,7 +4797,7 @@ dependencies = [ [[package]] name = "penumbra-bench" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ec", @@ -4841,7 +4841,7 @@ dependencies = [ [[package]] name = "penumbra-community-pool" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ff", @@ -4873,7 +4873,7 @@ dependencies = [ [[package]] name = "penumbra-compact-block" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ff", @@ -4909,7 +4909,7 @@ dependencies = [ [[package]] name = "penumbra-custody" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ff", @@ -4944,7 +4944,7 @@ dependencies = [ [[package]] name = "penumbra-dex" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ff", @@ -5004,7 +5004,7 @@ dependencies = [ [[package]] name = "penumbra-distributions" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "async-trait", @@ -5021,7 +5021,7 @@ dependencies = [ [[package]] name = "penumbra-eddy" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ff", @@ -5039,7 +5039,7 @@ dependencies = [ [[package]] name = "penumbra-fee" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ff", @@ -5064,7 +5064,7 @@ dependencies = [ [[package]] name = "penumbra-funding" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "async-trait", @@ -5086,7 +5086,7 @@ dependencies = [ [[package]] name = "penumbra-genesis" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "penumbra-community-pool", @@ -5105,7 +5105,7 @@ dependencies = [ [[package]] name = "penumbra-governance" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ff", @@ -5162,7 +5162,7 @@ dependencies = [ [[package]] name = "penumbra-ibc" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ff", @@ -5197,7 +5197,7 @@ dependencies = [ [[package]] name = "penumbra-keys" -version = "0.70.0" +version = "0.70.1" dependencies = [ "aes 0.8.4", "anyhow", @@ -5243,7 +5243,7 @@ dependencies = [ [[package]] name = "penumbra-measure" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "bytesize", @@ -5262,7 +5262,7 @@ dependencies = [ [[package]] name = "penumbra-mock-client" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "cnidarium", @@ -5278,7 +5278,7 @@ dependencies = [ [[package]] name = "penumbra-mock-consensus" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "bytes", @@ -5293,7 +5293,7 @@ dependencies = [ [[package]] name = "penumbra-num" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ff", @@ -5330,7 +5330,7 @@ dependencies = [ [[package]] name = "penumbra-proof-params" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ec", @@ -5359,7 +5359,7 @@ dependencies = [ [[package]] name = "penumbra-proof-setup" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ec", @@ -5387,7 +5387,7 @@ dependencies = [ [[package]] name = "penumbra-proto" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "async-trait", @@ -5417,7 +5417,7 @@ dependencies = [ [[package]] name = "penumbra-sct" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ff", @@ -5450,7 +5450,7 @@ dependencies = [ [[package]] name = "penumbra-shielded-pool" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ff", @@ -5500,7 +5500,7 @@ dependencies = [ [[package]] name = "penumbra-stake" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ff", @@ -5553,7 +5553,7 @@ dependencies = [ [[package]] name = "penumbra-tct" -version = "0.70.0" +version = "0.70.1" dependencies = [ "ark-ed-on-bls12-377", "ark-ff", @@ -5584,7 +5584,7 @@ dependencies = [ [[package]] name = "penumbra-tct-property-test" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "futures", @@ -5596,7 +5596,7 @@ dependencies = [ [[package]] name = "penumbra-tct-visualize" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "axum", @@ -5626,7 +5626,7 @@ dependencies = [ [[package]] name = "penumbra-tendermint-proxy" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "chrono", @@ -5657,7 +5657,7 @@ dependencies = [ [[package]] name = "penumbra-tower-trace" -version = "0.70.0" +version = "0.70.1" dependencies = [ "futures", "hex", @@ -5678,7 +5678,7 @@ dependencies = [ [[package]] name = "penumbra-transaction" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-ff", @@ -5730,7 +5730,7 @@ dependencies = [ [[package]] name = "penumbra-txhash" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "blake2b_simd 1.0.2", @@ -5742,7 +5742,7 @@ dependencies = [ [[package]] name = "penumbra-view" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-std", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "penumbra-wallet" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-std", @@ -7609,7 +7609,7 @@ checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" [[package]] name = "summonerd" -version = "0.70.0" +version = "0.70.1" dependencies = [ "anyhow", "ark-groth16", diff --git a/Cargo.toml b/Cargo.toml index 252042fdc7..2ff5599fb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,7 +98,7 @@ push = false [workspace.package] authors = ["Penumbra Labs "] edition = "2021" -version = "0.70.0" +version = "0.70.1" repository = "https://github.com/penumbra-zone/penumbra" homepage = "https://penumbra.zone" license = "MIT OR Apache-2.0" diff --git a/docs/guide/src/penumbra_version.md b/docs/guide/src/penumbra_version.md index 9ecab20b77..78a737c8d8 100644 --- a/docs/guide/src/penumbra_version.md +++ b/docs/guide/src/penumbra_version.md @@ -1 +1 @@ -v0.70.0 +v0.70.1 From 6897901b37f5c72d4b5c593167f0e0b933dd2eb5 Mon Sep 17 00:00:00 2001 From: Conor Schaefer Date: Mon, 25 Mar 2024 08:31:10 -0700 Subject: [PATCH 034/168] docs: chain upgrade procedure, for operators Adds documentation for the specific steps to be performed by a node operator in order to participate in a chain upgrade. These docs are largely based on the existing wiki notes [0], adapted for a generalized Penumbra setup. Similarly, sketched out some corresponding changes to the `pd testnet join` docs, but left them commented out for now: those docs are only relevant for joining a chain that has already been upgraded. If things go well with #4087, we'll uncomment those docs and start using them. Finishes and therefore closes #3841. [0] https://github.com/penumbra-zone/penumbra/wiki/Performing-upgrades --- docs/guide/src/SUMMARY.md | 1 + docs/guide/src/pd/chain-upgrade.md | 73 ++++++++++++++++++++++++++++++ docs/guide/src/pd/join-testnet.md | 23 ++++++++++ 3 files changed, 97 insertions(+) create mode 100644 docs/guide/src/pd/chain-upgrade.md diff --git a/docs/guide/src/SUMMARY.md b/docs/guide/src/SUMMARY.md index 8d2f28386f..dc9fce7f5d 100644 --- a/docs/guide/src/SUMMARY.md +++ b/docs/guide/src/SUMMARY.md @@ -12,6 +12,7 @@ - [Installing `pd`](./pd/install.md) - [Joining a testnet](./pd/join-testnet.md) - [Becoming a validator](./pd/validator.md) + - [Performing a chain upgrade](./pd/chain-upgrade.md) - [Debugging](./pd/debugging.md) - [Local RPC with `pclientd`](./pclientd.md) - [Configuring `pclientd`](./pclientd/configure.md) diff --git a/docs/guide/src/pd/chain-upgrade.md b/docs/guide/src/pd/chain-upgrade.md new file mode 100644 index 0000000000..da6233343d --- /dev/null +++ b/docs/guide/src/pd/chain-upgrade.md @@ -0,0 +1,73 @@ +# Performing chain upgrades + +When consensus-breaking changes are made to the Penumbra protocol, +node operators must coordinate upgrading to the new version of the software +at the same time. Penumbra uses a governance proposal for scheduling upgrades +at a specific block height. + +## Upgrade process abstractly + +At a high level, the upgrade process consists of the following steps: + +1. Governance proposal submitted, specifying explicit chain height `n` for halt to occur. +2. Governance proposal passes. +3. Chain reaches specified height `n-1`, nodes stop generating blocks. +4. Manual upgrade is performed on each validator and fullnode: + 1. Prepare migration directory via `pd export`. + 2. Install the new version of pd. + 3. Apply changes to node state via `pd migrate`. + 4. Copy a few files and directories around, clean up CometBFT state. + 5. Restart node. + +After the node is restarted on the new version, it should be able to talk to the network again. +Once enough validators with sufficient stake weight have upgraded, the network +will resume generating blocks. + + +## Genesis time + +In order for the chain to start again after the upgrade, all nodes must be using the same genesis information, +including the timestamp for the genesis event. While the `pd migrate` command will create a new `genesis.json` file, +it cannot know the correct genesis start time to use without the operator supplying the `--genesis-start` flag. +The community may choose to specify a start time within the upgrade proposal. If so, all operators must use that value +when performing the migration, as described below. Otherwise, validators must coordinate out of band to agree +on a genesis start time. + +Leveraging the governance proposal is the recommended way to solve this problem. If the genesis start time is a value +in the future, then after the upgrade is performed, the node will start, but not process blocks. It will wait +until the `--genesis-start` time is reached, at which point it will resume processing blocks. In this way, +the community of validators can coordinate resumption of chain activity, even when operators perform migrate their ndoes +at slightly different times. + +## Performing a chain upgrade + +The following steps assume that `pd` is using the default home directory of `~/.penumbra/testnet_data/node0/pd`. +If your instance is using a different directory, update the paths accordingly. + +1. Stop both `pd` and `cometbft`. Depending on how you run Penumbra, this could mean `sudo systemctl stop penumbra cometbft`. +2. Using the same version of `pd` that was running when the chain halted, prepare an export directory: + `pd export --home ~/.penumbra/testnet_data/node0/pd --export-directory ~/.penumbra/testnet_data/node0/pd-exported-state` +3. Back up the historical state directory: `mv ~/.penumbra/testnet_data/node0/pd ~/.penumbra/testnet_data/node0/pd-state-backup` +4. Download the latest version of `pd` and install it. Run `pd --version` and confirm you see `{{ #include ../penumbra_version.md }}` before proceeding. + + +5. Apply the migration: `pd migrate --genesis-start "GENESIS_TIME" --target-directory ~/.penumbra/testnet_net/node0/pd-exported-state/ --migrate-archive ~/.penumbra/testnet_data/node0/pd-migrated-state-{{ #include ../penumbra_version.md }}.tar.gz`. + Replace `GENESIS_TIME` with the exact string: `XXXXX`. +6. Move the migrated state into place: `mkdir ~/.penumbra/testnet_data/node0/pd && mv ~/.penumbra/testnet_data/node0/pd-exported-state/rocksdb ~/.penumbra/testnet_data/node0/pd/` +7. Move the upgrade cometbft state into place: `cp ~/.penumbra/testnet_data/node0/pd-exported-state/genesis.json ~/.penumbra/testnet_data/node0/cometbft/config/genesis.json + && cp ~/.penumbra/testnet_data/pd-exported-state/priv_validator_state.json ~/.penumbra/testnet_data/node0/cometbft/data/priv_validator_state.json` +8. Then we clean up the old CometBFT state: `find ~/.penumbra/testnet_data/node0/cometbft/data/ -mindepth 1 -maxdepth 1 -type d -exec rm -r {} +` + +Finally, restart the node, e.g. `sudo systemctl restart penumbra cometbft`. Check the logs, and you should see the chain progressing +past the halt height `n`. + +If you want to host a snapshot for this migration, copy the file +`~/.penumbra/testnet_data/node0/pd-migrated-state-{{ #include ../penumbra_version.md }}.tar.gz` to the appropriate hosting environment, +and inform the users of your validator. diff --git a/docs/guide/src/pd/join-testnet.md b/docs/guide/src/pd/join-testnet.md index ef5a38462e..1c42cad5e8 100644 --- a/docs/guide/src/pd/join-testnet.md +++ b/docs/guide/src/pd/join-testnet.md @@ -30,6 +30,26 @@ This will delete the entire testnet data directory. Next, generate a set of configs for the current testnet: + + ```shell pd testnet join --external-address IP_ADDRESS:26656 --moniker MY_NODE_NAME ``` @@ -37,6 +57,9 @@ pd testnet join --external-address IP_ADDRESS:26656 --moniker MY_NODE_NAME where `IP_ADDRESS` (like `1.2.3.4`) is the public IP address of the node you're running, and `MY_NODE_NAME` is a moniker identifying your node. Other peers will try to connect to your node over port `26656/TCP`. + If your node is behind a firewall or not publicly routable for some other reason, skip the `--external-address` flag, so that other peers won't try to connect to it. From 8b7f552665bce5c2131ef6c30c9d0f49f539b785 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Wed, 27 Mar 2024 15:22:14 -0400 Subject: [PATCH 035/168] proto(view): deprecate `start_epoch_index` (#4119) @jessepinho noticed that a submessage of the `TransactionPlannerRequest` envelope should be deprecated and replaced by a height field. This PR: - deprecate `TransactionPlannerRequest.UndelegateClaim.start_epoch_index` - adds `TransactionPlannerRequest.UndelegateClaim.unbonding_start_height` - regenerate the protos --- .../src/gen/proto_descriptor.bin.no_lfs | Bin 98383 -> 95074 bytes crates/proto/src/gen/penumbra.view.v1.rs | 4 ++++ .../proto/src/gen/penumbra.view.v1.serde.rs | 21 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 379528 -> 376419 bytes proto/penumbra/penumbra/view/v1/view.proto | 4 +++- 5 files changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs b/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs index 67d781308dc15349cc1633e0986b9be4d49fda1f..d50e1bd8818eb8b4d521b590c16703d42bad4e23 100644 GIT binary patch delta 19801 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 QsWnq;W;8wa^^V#931P9$$^ZZW diff --git a/crates/proto/src/gen/penumbra.view.v1.rs b/crates/proto/src/gen/penumbra.view.v1.rs index 8763c4218e..e84195a094 100644 --- a/crates/proto/src/gen/penumbra.view.v1.rs +++ b/crates/proto/src/gen/penumbra.view.v1.rs @@ -341,6 +341,7 @@ pub mod transaction_planner_request { super::super::super::core::keys::v1::IdentityKey, >, /// The epoch in which unbonding began, used to verify the penalty. + #[deprecated] #[prost(uint64, tag = "2")] pub start_epoch_index: u64, /// The penalty applied to undelegation, in bps^2 (10e-8). @@ -355,6 +356,9 @@ pub mod transaction_planner_request { pub unbonding_amount: ::core::option::Option< super::super::super::core::num::v1::Amount, >, + /// The height at which unbonding began. + #[prost(uint64, tag = "5")] + pub unbonding_start_height: u64, } impl ::prost::Name for UndelegateClaim { const NAME: &'static str = "UndelegateClaim"; diff --git a/crates/proto/src/gen/penumbra.view.v1.serde.rs b/crates/proto/src/gen/penumbra.view.v1.serde.rs index 8380503916..e0e980002f 100644 --- a/crates/proto/src/gen/penumbra.view.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.view.v1.serde.rs @@ -7038,6 +7038,9 @@ impl serde::Serialize for transaction_planner_request::UndelegateClaim { if self.unbonding_amount.is_some() { len += 1; } + if self.unbonding_start_height != 0 { + len += 1; + } let mut struct_ser = serializer.serialize_struct("penumbra.view.v1.TransactionPlannerRequest.UndelegateClaim", len)?; if let Some(v) = self.validator_identity.as_ref() { struct_ser.serialize_field("validatorIdentity", v)?; @@ -7052,6 +7055,10 @@ impl serde::Serialize for transaction_planner_request::UndelegateClaim { if let Some(v) = self.unbonding_amount.as_ref() { struct_ser.serialize_field("unbondingAmount", v)?; } + if self.unbonding_start_height != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("unbondingStartHeight", ToString::to_string(&self.unbonding_start_height).as_str())?; + } struct_ser.end() } } @@ -7069,6 +7076,8 @@ impl<'de> serde::Deserialize<'de> for transaction_planner_request::UndelegateCla "penalty", "unbonding_amount", "unbondingAmount", + "unbonding_start_height", + "unbondingStartHeight", ]; #[allow(clippy::enum_variant_names)] @@ -7077,6 +7086,7 @@ impl<'de> serde::Deserialize<'de> for transaction_planner_request::UndelegateCla StartEpochIndex, Penalty, UnbondingAmount, + UnbondingStartHeight, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -7103,6 +7113,7 @@ impl<'de> serde::Deserialize<'de> for transaction_planner_request::UndelegateCla "startEpochIndex" | "start_epoch_index" => Ok(GeneratedField::StartEpochIndex), "penalty" => Ok(GeneratedField::Penalty), "unbondingAmount" | "unbonding_amount" => Ok(GeneratedField::UnbondingAmount), + "unbondingStartHeight" | "unbonding_start_height" => Ok(GeneratedField::UnbondingStartHeight), _ => Ok(GeneratedField::__SkipField__), } } @@ -7126,6 +7137,7 @@ impl<'de> serde::Deserialize<'de> for transaction_planner_request::UndelegateCla let mut start_epoch_index__ = None; let mut penalty__ = None; let mut unbonding_amount__ = None; + let mut unbonding_start_height__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::ValidatorIdentity => { @@ -7154,6 +7166,14 @@ impl<'de> serde::Deserialize<'de> for transaction_planner_request::UndelegateCla } unbonding_amount__ = map_.next_value()?; } + GeneratedField::UnbondingStartHeight => { + if unbonding_start_height__.is_some() { + return Err(serde::de::Error::duplicate_field("unbondingStartHeight")); + } + unbonding_start_height__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -7164,6 +7184,7 @@ impl<'de> serde::Deserialize<'de> for transaction_planner_request::UndelegateCla start_epoch_index: start_epoch_index__.unwrap_or_default(), penalty: penalty__, unbonding_amount: unbonding_amount__, + unbonding_start_height: unbonding_start_height__.unwrap_or_default(), }) } } diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index a631b5d5d476d7e4523fb183a801603fee5ad029..45cac790c0812776b6a1b6906be3c49ead01d25d 100644 GIT binary patch delta 27528 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 M>8-y0pH|xcA8!>4%>V!Z delta 30536 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 R0D^Iv-LCyeM_qpYe*sv%ad-d# diff --git a/proto/penumbra/penumbra/view/v1/view.proto b/proto/penumbra/penumbra/view/v1/view.proto index ab46b161b6..de21d865b7 100644 --- a/proto/penumbra/penumbra/view/v1/view.proto +++ b/proto/penumbra/penumbra/view/v1/view.proto @@ -260,13 +260,15 @@ message TransactionPlannerRequest { // The identity key of the validator to finish undelegating from. core.keys.v1.IdentityKey validator_identity = 1; // The epoch in which unbonding began, used to verify the penalty. - uint64 start_epoch_index = 2; + uint64 start_epoch_index = 2 [deprecated = true]; // The penalty applied to undelegation, in bps^2 (10e-8). // In the happy path (no slashing), this is 0. core.component.stake.v1.Penalty penalty = 3; // The amount of unbonding tokens to claim. // This is a bare number because its denom is determined by the preceding data. core.num.v1.Amount unbonding_amount = 4; + // The height at which unbonding began. + uint64 unbonding_start_height = 5; } message PositionOpen { // Contains the data defining the position, sufficient to compute its `PositionId`. From fe0202b79c85ae02ab16b81636d22d40bde3f297 Mon Sep 17 00:00:00 2001 From: Conor Schaefer Date: Wed, 27 Mar 2024 12:29:53 -0700 Subject: [PATCH 036/168] chore: release version 0.70.2 --- Cargo.lock | 88 +++++++++++++++--------------- Cargo.toml | 2 +- docs/guide/src/penumbra_version.md | 2 +- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d49c1818bd..e06cf59c38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1378,7 +1378,7 @@ dependencies = [ [[package]] name = "cnidarium" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "async-trait", @@ -1413,7 +1413,7 @@ dependencies = [ [[package]] name = "cnidarium-component" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "async-trait", @@ -1892,7 +1892,7 @@ dependencies = [ [[package]] name = "decaf377-fmd" -version = "0.70.1" +version = "0.70.2" dependencies = [ "ark-ff", "ark-serialize", @@ -1907,7 +1907,7 @@ dependencies = [ [[package]] name = "decaf377-frost" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ff", @@ -1922,7 +1922,7 @@ dependencies = [ [[package]] name = "decaf377-ka" -version = "0.70.1" +version = "0.70.2" dependencies = [ "ark-ff", "decaf377 0.5.0", @@ -4397,7 +4397,7 @@ dependencies = [ [[package]] name = "pcli" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ff", @@ -4475,7 +4475,7 @@ dependencies = [ [[package]] name = "pclientd" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "assert_cmd", @@ -4527,7 +4527,7 @@ dependencies = [ [[package]] name = "pd" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ff", @@ -4666,7 +4666,7 @@ dependencies = [ [[package]] name = "penumbra-app" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ff", @@ -4746,7 +4746,7 @@ dependencies = [ [[package]] name = "penumbra-asset" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ff", @@ -4785,7 +4785,7 @@ dependencies = [ [[package]] name = "penumbra-auto-https" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "axum-server", @@ -4797,7 +4797,7 @@ dependencies = [ [[package]] name = "penumbra-bench" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ec", @@ -4841,7 +4841,7 @@ dependencies = [ [[package]] name = "penumbra-community-pool" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ff", @@ -4873,7 +4873,7 @@ dependencies = [ [[package]] name = "penumbra-compact-block" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ff", @@ -4909,7 +4909,7 @@ dependencies = [ [[package]] name = "penumbra-custody" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ff", @@ -4944,7 +4944,7 @@ dependencies = [ [[package]] name = "penumbra-dex" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ff", @@ -5004,7 +5004,7 @@ dependencies = [ [[package]] name = "penumbra-distributions" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "async-trait", @@ -5021,7 +5021,7 @@ dependencies = [ [[package]] name = "penumbra-eddy" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ff", @@ -5039,7 +5039,7 @@ dependencies = [ [[package]] name = "penumbra-fee" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ff", @@ -5064,7 +5064,7 @@ dependencies = [ [[package]] name = "penumbra-funding" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "async-trait", @@ -5086,7 +5086,7 @@ dependencies = [ [[package]] name = "penumbra-genesis" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "penumbra-community-pool", @@ -5105,7 +5105,7 @@ dependencies = [ [[package]] name = "penumbra-governance" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ff", @@ -5162,7 +5162,7 @@ dependencies = [ [[package]] name = "penumbra-ibc" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ff", @@ -5197,7 +5197,7 @@ dependencies = [ [[package]] name = "penumbra-keys" -version = "0.70.1" +version = "0.70.2" dependencies = [ "aes 0.8.4", "anyhow", @@ -5243,7 +5243,7 @@ dependencies = [ [[package]] name = "penumbra-measure" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "bytesize", @@ -5262,7 +5262,7 @@ dependencies = [ [[package]] name = "penumbra-mock-client" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "cnidarium", @@ -5278,7 +5278,7 @@ dependencies = [ [[package]] name = "penumbra-mock-consensus" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "bytes", @@ -5293,7 +5293,7 @@ dependencies = [ [[package]] name = "penumbra-num" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ff", @@ -5330,7 +5330,7 @@ dependencies = [ [[package]] name = "penumbra-proof-params" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ec", @@ -5359,7 +5359,7 @@ dependencies = [ [[package]] name = "penumbra-proof-setup" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ec", @@ -5387,7 +5387,7 @@ dependencies = [ [[package]] name = "penumbra-proto" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "async-trait", @@ -5417,7 +5417,7 @@ dependencies = [ [[package]] name = "penumbra-sct" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ff", @@ -5450,7 +5450,7 @@ dependencies = [ [[package]] name = "penumbra-shielded-pool" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ff", @@ -5500,7 +5500,7 @@ dependencies = [ [[package]] name = "penumbra-stake" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ff", @@ -5553,7 +5553,7 @@ dependencies = [ [[package]] name = "penumbra-tct" -version = "0.70.1" +version = "0.70.2" dependencies = [ "ark-ed-on-bls12-377", "ark-ff", @@ -5584,7 +5584,7 @@ dependencies = [ [[package]] name = "penumbra-tct-property-test" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "futures", @@ -5596,7 +5596,7 @@ dependencies = [ [[package]] name = "penumbra-tct-visualize" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "axum", @@ -5626,7 +5626,7 @@ dependencies = [ [[package]] name = "penumbra-tendermint-proxy" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "chrono", @@ -5657,7 +5657,7 @@ dependencies = [ [[package]] name = "penumbra-tower-trace" -version = "0.70.1" +version = "0.70.2" dependencies = [ "futures", "hex", @@ -5678,7 +5678,7 @@ dependencies = [ [[package]] name = "penumbra-transaction" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-ff", @@ -5730,7 +5730,7 @@ dependencies = [ [[package]] name = "penumbra-txhash" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "blake2b_simd 1.0.2", @@ -5742,7 +5742,7 @@ dependencies = [ [[package]] name = "penumbra-view" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-std", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "penumbra-wallet" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-std", @@ -7609,7 +7609,7 @@ checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" [[package]] name = "summonerd" -version = "0.70.1" +version = "0.70.2" dependencies = [ "anyhow", "ark-groth16", diff --git a/Cargo.toml b/Cargo.toml index 2ff5599fb5..4e6a99dc1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,7 +98,7 @@ push = false [workspace.package] authors = ["Penumbra Labs "] edition = "2021" -version = "0.70.1" +version = "0.70.2" repository = "https://github.com/penumbra-zone/penumbra" homepage = "https://penumbra.zone" license = "MIT OR Apache-2.0" diff --git a/docs/guide/src/penumbra_version.md b/docs/guide/src/penumbra_version.md index 78a737c8d8..56636a4aa1 100644 --- a/docs/guide/src/penumbra_version.md +++ b/docs/guide/src/penumbra_version.md @@ -1 +1 @@ -v0.70.1 +v0.70.2 From 9c3bc988c276387564b57ce35808a1a2c9539b20 Mon Sep 17 00:00:00 2001 From: Henry de Valence Date: Wed, 27 Mar 2024 15:31:11 -0700 Subject: [PATCH 037/168] Improve `update_position` API and position execution events. (#4121) --- .../src/gen/proto_descriptor.bin.no_lfs | Bin 95074 -> 98383 bytes .../src/component/circuit_breaker/value.rs | 2 +- .../core/component/dex/src/component/dex.rs | 3 +- .../dex/src/component/position_manager.rs | 178 +++++++++++------- .../dex/src/component/router/fill_route.rs | 18 +- .../dex/src/component/router/path.rs | 3 +- .../dex/src/component/router/tests.rs | 8 +- crates/core/component/dex/src/event.rs | 19 +- crates/core/component/dex/src/lp/position.rs | 2 +- crates/core/component/dex/src/lp/reserves.rs | 2 +- .../src/gen/penumbra.core.component.dex.v1.rs | 9 + .../penumbra.core.component.dex.v1.serde.rs | 53 ++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 376419 -> 380322 bytes .../penumbra/core/component/dex/v1/dex.proto | 6 + 14 files changed, 218 insertions(+), 85 deletions(-) diff --git a/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs b/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs index d50e1bd8818eb8b4d521b590c16703d42bad4e23..67d781308dc15349cc1633e0986b9be4d49fda1f 100644 GIT binary patch delta 23133 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 QsWnq;W;8wa^^V#931P9$$^ZZW delta 19801 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 Result<()> { - match self.get_raw(&state_key::position_by_id(id)).await? { - Some(_) => Err(anyhow::anyhow!("position id {:?} already used", id)), - None => Ok(()), - } - } - async fn best_position( &self, pair: &DirectedTradingPair, @@ -140,19 +133,37 @@ impl PositionRead for T {} #[async_trait] pub trait PositionManager: StateWrite + PositionRead { /// Close a position by id, removing it from the state. + /// + /// If the position is already closed, this is a no-op. + /// /// # Errors + /// /// Returns an error if the position does not exist. async fn close_position_by_id(&mut self, id: &position::Id) -> Result<()> { tracing::debug!(?id, "closing position, first fetch it"); - let mut position = self + let prev_state = self .position_by_id(id) .await .expect("fetching position should not fail") - .ok_or_else(|| anyhow::anyhow!("position not found"))?; + .ok_or_else(|| anyhow::anyhow!("could not find position {} to close", id))?; + + anyhow::ensure!( + matches!( + prev_state.state, + position::State::Opened | position::State::Closed, + ), + "attempted to close a position with state {:?}, expected Opened or Closed", + prev_state.state + ); + + let new_state = { + let mut new_state = prev_state.clone(); + new_state.state = position::State::Closed; + new_state + }; + + self.update_position(Some(prev_state), new_state).await?; - tracing::debug!(?id, "position found, close it"); - position.state = position::State::Closed; - self.update_position(position).await?; Ok(()) } @@ -164,18 +175,13 @@ pub trait PositionManager: StateWrite + PositionRead { } /// Close all positions that have been queued for closure. - async fn close_queued_positions(&mut self) -> () { + async fn close_queued_positions(&mut self) -> Result<()> { let to_close = self.pending_position_closures(); for id in to_close { - match self.close_position_by_id(&id).await { - Ok(()) => tracing::debug!(?id, "position closed"), - // The position was already closed, which in and of itself is not an error. - // It's possible that the position was closed by the engine, for example - // because it was a limit-order. - Err(e) => tracing::debug!(?id, "failed to close position: {}", e), - } + self.close_position_by_id(&id).await?; } self.object_delete(state_key::pending_position_closures()); + Ok(()) } /// Opens a new position, updating all necessary indexes and checking for @@ -188,7 +194,13 @@ pub trait PositionManager: StateWrite + PositionRead { } // Validate that the position ID doesn't collide - self.check_position_id_unused(&position.id()).await?; + if let Some(existing) = self.position_by_id(&position.id()).await? { + anyhow::bail!( + "attempted to open a position with ID {}, which already exists with state {:?}", + position.id(), + existing + ); + } // Credit the DEX for the inflows from this position. self.vcb_credit(position.reserves_1()).await?; @@ -196,30 +208,65 @@ pub trait PositionManager: StateWrite + PositionRead { // Finally, record the new position state. self.record_proto(event::position_open(&position)); - self.update_position(position).await?; + self.update_position(None, position).await?; Ok(()) } /// Record execution against an opened position. + /// + /// The `context` parameter records the global context of the path in which + /// the position execution happened. This may be completely different than + /// the trading pair of the position itself, and is used to link the + /// micro-scale execution (processed by this method) with the macro-scale + /// context (a swap or arbitrage). #[tracing::instrument(level = "debug", skip_all)] - async fn position_execution(&mut self, mut position: Position) -> Result<()> { + async fn position_execution( + &mut self, + mut new_state: Position, + context: DirectedTradingPair, + ) -> Result<()> { + let prev_state = self + .position_by_id(&new_state.id()) + .await? + .ok_or_else(|| anyhow::anyhow!("withdrew from unknown position {}", new_state.id()))?; + + anyhow::ensure!( + matches!(&prev_state.state, position::State::Opened), + "attempted to execute against a position with state {:?}, expected Opened", + prev_state.state + ); + anyhow::ensure!( + matches!(&new_state.state, position::State::Opened), + "supplied post-execution state {:?}, expected Opened", + prev_state.state + ); + // Handle "close-on-fill": automatically flip the position state to "closed" if // either of the reserves are zero. - if position.close_on_fill { - if position.reserves.r1 == 0u64.into() || position.reserves.r2 == 0u64.into() { + if new_state.close_on_fill { + if new_state.reserves.r1 == 0u64.into() || new_state.reserves.r2 == 0u64.into() { tracing::debug!( - id = ?position.id(), - r1 = ?position.reserves.r1, - r2 = ?position.reserves.r2, + id = ?new_state.id(), + r1 = ?new_state.reserves.r1, + r2 = ?new_state.reserves.r2, "marking position as closed due to close-on-fill" ); - position.state = position::State::Closed; + new_state.state = position::State::Closed; } } - self.record_proto(event::position_execution(&position)); - self.update_position(position).await?; + // Optimization: it's possible that the position's reserves haven't + // changed, and that we're about to do a no-op update. This can happen + // when saving a frontier, for instance, since the FillRoute code saves + // the entire frontier when it finishes. + // + // If so, skip the write, but more importantly, skip emitting an event, + // so tooling doesn't get confused about a no-op execution. + if prev_state != new_state { + self.record_proto(event::position_execution(&prev_state, &new_state, context)); + self.update_position(Some(prev_state), new_state).await?; + } Ok(()) } @@ -233,7 +280,7 @@ pub trait PositionManager: StateWrite + PositionRead { position_id: position::Id, sequence: u64, ) -> Result { - let mut metadata = self + let prev_state = self .position_by_id(&position_id) .await? .ok_or_else(|| anyhow::anyhow!("withdrew from unknown position {}", position_id))?; @@ -246,17 +293,17 @@ pub trait PositionManager: StateWrite + PositionRead { // This is just a check that sequence == current_sequence + 1, with extra logic // so that we treat "closed" as "sequence -1". if sequence == 0 { - if metadata.state != position::State::Closed { + if prev_state.state != position::State::Closed { anyhow::bail!( "attempted to withdraw position {} with state {}, expected Closed", position_id, - metadata.state + prev_state.state ); } } else { if let position::State::Withdrawn { sequence: current_sequence, - } = metadata.state + } = prev_state.state { if current_sequence + 1 != sequence { anyhow::bail!( @@ -270,34 +317,34 @@ pub trait PositionManager: StateWrite + PositionRead { anyhow::bail!( "attempted to withdraw position {} with state {}, expected Withdrawn", position_id, - metadata.state + prev_state.state ); } } // Record an event prior to updating the position state, so we have access to // the current reserves. - self.record_proto(event::position_withdraw(position_id, &metadata)); + self.record_proto(event::position_withdraw(position_id, &prev_state)); // Grab a copy of the final reserves of the position to return to the caller. - let reserves = metadata.reserves.balance(&metadata.phi.pair); + let reserves = prev_state.reserves.balance(&prev_state.phi.pair); // Debit the DEX for the outflows from this position. - // TODO: in a future PR, split current PositionManager to PositionManagerInner - // and fold this into a position open method - self.vcb_debit(metadata.reserves_1()).await?; - self.vcb_debit(metadata.reserves_2()).await?; + self.vcb_debit(prev_state.reserves_1()).await?; + self.vcb_debit(prev_state.reserves_2()).await?; // Finally, update the position. This has two steps: // - update the state with the correct sequence number; // - zero out the reserves, to prevent double-withdrawals. - metadata.state = position::State::Withdrawn { + let new_state = { + let mut new_state = prev_state.clone(); // We just checked that the supplied sequence number is incremented by 1 from prev. - sequence, + new_state.state = position::State::Withdrawn { sequence }; + new_state.reserves = Reserves::zero(); + new_state }; - metadata.reserves = Reserves::zero(); - self.update_position(metadata).await?; + self.update_position(Some(prev_state), new_state).await?; Ok(reserves) } @@ -311,36 +358,38 @@ pub(crate) trait Inner: StateWrite { /// /// This should be the SOLE ENTRYPOINT for writing positions to the state. /// All other position changes exposed by the `PositionManager` should run through here. - #[tracing::instrument(level = "debug", skip(self, position), fields(id = ?position.id()))] - async fn update_position(&mut self, position: position::Position) -> Result<()> { - let id = position.id(); - tracing::debug!(?position, "fetch position's previous state from storage"); - // We pull the position from the state unconditionally, since we will - // always need to update the position's liquidity index. - let prev = self - .position_by_id(&id) - .await - .expect("fetching position should not fail"); + #[tracing::instrument(level = "debug", skip_all, fields(id = ?new_state.id()))] + async fn update_position( + &mut self, + prev_state: Option, + new_state: Position, + ) -> Result<()> { + tracing::debug!(?prev_state, ?new_state, "updating position state"); + + let id = new_state.id(); // Clear any existing indexes of the position, since changes to the // reserves or the position state might have invalidated them. - self.deindex_position_by_price(&position); + if let Some(prev_state) = prev_state.as_ref() { + self.deindex_position_by_price(&prev_state, &id); + } // Only index the position's liquidity if it is active. - if position.state == position::State::Opened { - self.index_position_by_price(&position); + if new_state.state == position::State::Opened { + self.index_position_by_price(&new_state, &id); } // Update the available liquidity for this position's trading pair. - self.update_available_liquidity(&position, &prev).await?; + // TODO: refactor and streamline this method while implementing eviction. + self.update_available_liquidity(&new_state, &prev_state) + .await?; - self.put(state_key::position_by_id(&id), position); + self.put(state_key::position_by_id(&id), new_state); Ok(()) } - fn index_position_by_price(&mut self, position: &position::Position) { + fn index_position_by_price(&mut self, position: &position::Position, id: &position::Id) { let (pair, phi) = (position.phi.pair, &position.phi); - let id = position.id(); if position.reserves.r2 != 0u64.into() { // Index this position for trades FROM asset 1 TO asset 2, since the position has asset 2 to give out. let pair12 = DirectedTradingPair { @@ -370,8 +419,7 @@ pub(crate) trait Inner: StateWrite { } } - fn deindex_position_by_price(&mut self, position: &Position) { - let id = position.id(); + fn deindex_position_by_price(&mut self, position: &Position, id: &position::Id) { tracing::debug!("deindexing position"); let pair12 = DirectedTradingPair { start: position.phi.pair.asset_1(), diff --git a/crates/core/component/dex/src/component/router/fill_route.rs b/crates/core/component/dex/src/component/router/fill_route.rs index 1f8ea5e1a1..ac35ab746a 100644 --- a/crates/core/component/dex/src/component/router/fill_route.rs +++ b/crates/core/component/dex/src/component/router/fill_route.rs @@ -12,12 +12,10 @@ use penumbra_num::{ fixpoint::{Error, U128x128}, Amount, }; -use penumbra_proto::StateWriteProto as _; use tracing::instrument; use crate::{ component::{metrics, PositionManager, PositionRead}, - event, lp::{ position::{self, Position}, Reserves, @@ -411,12 +409,14 @@ impl Frontier { } async fn save(&mut self) -> Result<()> { + let context = DirectedTradingPair { + start: self.pairs.first().expect("pairs is nonempty").start, + end: self.pairs.last().expect("pairs is nonempty").end, + }; for position in &self.positions { - self.state.position_execution(position.clone()).await?; - - // Create an ABCI event signaling that the position was executed against self.state - .record_proto(event::position_execution(&position)); + .position_execution(position.clone(), context.clone()) + .await?; } Ok(()) } @@ -491,8 +491,12 @@ impl Frontier { // discard it, so write its updated reserves before we replace it on the // frontier. The other positions will be written out either when // they're fully consumed, or when we finish filling. + let context = DirectedTradingPair { + start: self.pairs.first().expect("pairs is nonempty").start, + end: self.pairs.last().expect("pairs is nonempty").end, + }; self.state - .position_execution(self.positions[index].clone()) + .position_execution(self.positions[index].clone(), context) .await .expect("writing to storage should not fail"); diff --git a/crates/core/component/dex/src/component/router/path.rs b/crates/core/component/dex/src/component/router/path.rs index 330bc2222f..a9fd283590 100644 --- a/crates/core/component/dex/src/component/router/path.rs +++ b/crates/core/component/dex/src/component/router/path.rs @@ -74,7 +74,8 @@ impl Path { // Deindex the position we "consumed" in this and all descendant state forks, // ensuring we don't double-count liquidity while traversing cycles. use super::super::position_manager::Inner as _; - self.state.deindex_position_by_price(&best_price_position); + self.state + .deindex_position_by_price(&best_price_position, &best_price_position.id()); // Compute the effective price of a trade in the direction self.end()=>new_end let hop_price = best_price_position diff --git a/crates/core/component/dex/src/component/router/tests.rs b/crates/core/component/dex/src/component/router/tests.rs index 6618d902ec..bd9c5f4de6 100644 --- a/crates/core/component/dex/src/component/router/tests.rs +++ b/crates/core/component/dex/src/component/router/tests.rs @@ -496,6 +496,10 @@ async fn test_multiple_similar_position() -> anyhow::Result<()> { state_tx.open_position(buy_1.clone()).await.unwrap(); state_tx.open_position(buy_2.clone()).await.unwrap(); + // We don't really care about the value, but the API requires + // that we make up a context here. + let context = pair_1.into_directed_trading_pair(); + let mut p_1 = state_tx .best_position(&pair_1.into_directed_trading_pair()) .await @@ -503,7 +507,7 @@ async fn test_multiple_similar_position() -> anyhow::Result<()> { .expect("we just posted two positions"); assert_eq!(p_1.nonce, buy_1.nonce); p_1.reserves = p_1.reserves.flip(); - state_tx.position_execution(p_1).await.unwrap(); + state_tx.position_execution(p_1, context).await.unwrap(); let mut p_2 = state_tx .best_position(&pair_1.into_directed_trading_pair()) @@ -512,7 +516,7 @@ async fn test_multiple_similar_position() -> anyhow::Result<()> { .expect("there is one position remaining"); assert_eq!(p_2.nonce, buy_2.nonce); p_2.reserves = p_2.reserves.flip(); - state_tx.position_execution(p_2).await.unwrap(); + state_tx.position_execution(p_2, context).await.unwrap(); assert!(state_tx .best_position(&pair_1.into_directed_trading_pair()) diff --git a/crates/core/component/dex/src/event.rs b/crates/core/component/dex/src/event.rs index 22832b2bf9..8aae026868 100644 --- a/crates/core/component/dex/src/event.rs +++ b/crates/core/component/dex/src/event.rs @@ -5,7 +5,7 @@ use crate::{ }, swap::Swap, swap_claim::SwapClaim, - BatchSwapOutputData, SwapExecution, + BatchSwapOutputData, DirectedTradingPair, SwapExecution, }; use penumbra_asset::asset; @@ -66,12 +66,19 @@ pub fn position_withdraw( } } -pub fn position_execution(post_execution_state: &Position) -> pb::EventPositionExecution { +pub fn position_execution( + prev_state: &Position, + new_state: &Position, + context: DirectedTradingPair, +) -> pb::EventPositionExecution { pb::EventPositionExecution { - position_id: Some(post_execution_state.id().into()), - trading_pair: Some(post_execution_state.phi.pair.into()), - reserves_1: Some(post_execution_state.reserves.r1.into()), - reserves_2: Some(post_execution_state.reserves.r2.into()), + position_id: Some(new_state.id().into()), + trading_pair: Some(new_state.phi.pair.into()), + reserves_1: Some(new_state.reserves.r1.into()), + reserves_2: Some(new_state.reserves.r2.into()), + prev_reserves_1: Some(prev_state.reserves.r1.into()), + prev_reserves_2: Some(prev_state.reserves.r2.into()), + context: Some(context.into()), } } diff --git a/crates/core/component/dex/src/lp/position.rs b/crates/core/component/dex/src/lp/position.rs index 05dabc0d74..1e2e9ebbc4 100644 --- a/crates/core/component/dex/src/lp/position.rs +++ b/crates/core/component/dex/src/lp/position.rs @@ -19,7 +19,7 @@ pub const MAX_FEE_BPS: u32 = 5000; /// Encapsulates the immutable parts of the position (phi/nonce), along /// with the mutable parts (state/reserves). -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(try_from = "pb::Position", into = "pb::Position")] pub struct Position { pub state: State, diff --git a/crates/core/component/dex/src/lp/reserves.rs b/crates/core/component/dex/src/lp/reserves.rs index 265603ea3c..215d12aa5c 100644 --- a/crates/core/component/dex/src/lp/reserves.rs +++ b/crates/core/component/dex/src/lp/reserves.rs @@ -12,7 +12,7 @@ use super::position::MAX_RESERVE_AMOUNT; /// between assets 1 and 2, without specifying what those assets are, to avoid /// duplicating data (each asset ID alone is four times the size of the /// reserves). -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Reserves { pub r1: Amount, pub r2: Amount, diff --git a/crates/proto/src/gen/penumbra.core.component.dex.v1.rs b/crates/proto/src/gen/penumbra.core.component.dex.v1.rs index 27e6a5c183..316658f996 100644 --- a/crates/proto/src/gen/penumbra.core.component.dex.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.dex.v1.rs @@ -1399,6 +1399,15 @@ pub struct EventPositionExecution { /// The reserves of asset 2 of the position after execution. #[prost(message, optional, tag = "4")] pub reserves_2: ::core::option::Option, + /// The reserves of asset 1 of the position before execution. + #[prost(message, optional, tag = "5")] + pub prev_reserves_1: ::core::option::Option, + /// The reserves of asset 2 of the position before execution. + #[prost(message, optional, tag = "6")] + pub prev_reserves_2: ::core::option::Option, + /// Context: the end-to-end route that was being traversed during execution. + #[prost(message, optional, tag = "7")] + pub context: ::core::option::Option, } impl ::prost::Name for EventPositionExecution { const NAME: &'static str = "EventPositionExecution"; diff --git a/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs index 14e166d4f8..b7294296fc 100644 --- a/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs @@ -1645,6 +1645,15 @@ impl serde::Serialize for EventPositionExecution { if self.reserves_2.is_some() { len += 1; } + if self.prev_reserves_1.is_some() { + len += 1; + } + if self.prev_reserves_2.is_some() { + len += 1; + } + if self.context.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("penumbra.core.component.dex.v1.EventPositionExecution", len)?; if let Some(v) = self.position_id.as_ref() { struct_ser.serialize_field("positionId", v)?; @@ -1658,6 +1667,15 @@ impl serde::Serialize for EventPositionExecution { if let Some(v) = self.reserves_2.as_ref() { struct_ser.serialize_field("reserves2", v)?; } + if let Some(v) = self.prev_reserves_1.as_ref() { + struct_ser.serialize_field("prevReserves1", v)?; + } + if let Some(v) = self.prev_reserves_2.as_ref() { + struct_ser.serialize_field("prevReserves2", v)?; + } + if let Some(v) = self.context.as_ref() { + struct_ser.serialize_field("context", v)?; + } struct_ser.end() } } @@ -1676,6 +1694,11 @@ impl<'de> serde::Deserialize<'de> for EventPositionExecution { "reserves1", "reserves_2", "reserves2", + "prev_reserves_1", + "prevReserves1", + "prev_reserves_2", + "prevReserves2", + "context", ]; #[allow(clippy::enum_variant_names)] @@ -1684,6 +1707,9 @@ impl<'de> serde::Deserialize<'de> for EventPositionExecution { TradingPair, Reserves1, Reserves2, + PrevReserves1, + PrevReserves2, + Context, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -1710,6 +1736,9 @@ impl<'de> serde::Deserialize<'de> for EventPositionExecution { "tradingPair" | "trading_pair" => Ok(GeneratedField::TradingPair), "reserves1" | "reserves_1" => Ok(GeneratedField::Reserves1), "reserves2" | "reserves_2" => Ok(GeneratedField::Reserves2), + "prevReserves1" | "prev_reserves_1" => Ok(GeneratedField::PrevReserves1), + "prevReserves2" | "prev_reserves_2" => Ok(GeneratedField::PrevReserves2), + "context" => Ok(GeneratedField::Context), _ => Ok(GeneratedField::__SkipField__), } } @@ -1733,6 +1762,9 @@ impl<'de> serde::Deserialize<'de> for EventPositionExecution { let mut trading_pair__ = None; let mut reserves_1__ = None; let mut reserves_2__ = None; + let mut prev_reserves_1__ = None; + let mut prev_reserves_2__ = None; + let mut context__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::PositionId => { @@ -1759,6 +1791,24 @@ impl<'de> serde::Deserialize<'de> for EventPositionExecution { } reserves_2__ = map_.next_value()?; } + GeneratedField::PrevReserves1 => { + if prev_reserves_1__.is_some() { + return Err(serde::de::Error::duplicate_field("prevReserves1")); + } + prev_reserves_1__ = map_.next_value()?; + } + GeneratedField::PrevReserves2 => { + if prev_reserves_2__.is_some() { + return Err(serde::de::Error::duplicate_field("prevReserves2")); + } + prev_reserves_2__ = map_.next_value()?; + } + GeneratedField::Context => { + if context__.is_some() { + return Err(serde::de::Error::duplicate_field("context")); + } + context__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -1769,6 +1819,9 @@ impl<'de> serde::Deserialize<'de> for EventPositionExecution { trading_pair: trading_pair__, reserves_1: reserves_1__, reserves_2: reserves_2__, + prev_reserves_1: prev_reserves_1__, + prev_reserves_2: prev_reserves_2__, + context: context__, }) } } diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 45cac790c0812776b6a1b6906be3c49ead01d25d..8a26d29b046550f594d789bdc692d546f5431d8b 100644 GIT binary patch delta 24735 zcmb7s33OFOwsxw{bnnT0k^lyfn=p?us0|Xd5rU#f08td44I#Nex!v>`*4?|qbxJBLYN8L+G;#b%vyC=S z-SlpDNXs@}{`g5ZdF2u2I-Zr3w9E2`yk}-_T>Hhw682Qxn1SrCEPbGBnmxJcne#2) zDz~>k|5n!6uegnQMPsAmDyyQA=mXJ`$u(t_6BSj6 zvNGBzJqEp{R{IW0k9+2F(!1-{<$(%^%JM*kLuEOsP*ZO3>}Rc{RjMgBpyabw=qmRn zSaAb2z}vs^4%L?N^j48(MnopvD{An=pxq0KXV6bk^Z6;YJ|qX+yo)YykR++8iMT&D?7*SB~-Yv;S;inv(K`3fJh{BaOQ_rU|J41~B-T8fc{%L51ER$>ldGb4Mr*vsn15y}yZQEBk1)1`nM)XZ zo~3%B!h41c8rW~(@Foq8zyt44_)Cf7&rdkRQpc4=%S(sQ>#L$9>W%ZqJji->Z7B$C zL742>{RS1?S=e9Dg*~%qNa5fnUpm7=(Td3vhfkdp^%nTs9%RGYwf&hHeFyhfdSDsS zx8LnRUpjuL-?ApT54h=*YsU2)eSdLzS!uE808==jgP%8vH6769EI!T|${ zhDkrWp4E5IprR3Eh5PI3%8CJ%RTGPA{O=!PXRhk!rv0=vUQ@U6UBd_T^fRWhMmZr@ zG$(_3uB#f8!TnpOvAQ0&*`Y6;j1x}&74CXL*Q*Gs_U>^*r81hTo3Mz{F1dSwa9>Q9 z=n^RXH2Q|c6}`T0`oyw|vWb%?j@DyAcE!IP3&YZsg5neu#Sa7&F^nhc{~godkQ$03 zcQ$jI{5xy^7h`1@4QP`B%C{%8UUffCW546E4kxi}($;3V>4p6V4j))Fc=TO^Zy#JV zVsJ(hUUq3(IAmDS@S^S+4bIPmJu<#@yi7Mmm+aoPdk=Jv*VxU{qHd#yQ*0Cs9&|5y zR6U4MUELF&aId-BSgZ8Hf4@k0_{==}Umy2Oc>L_oANNXlykeex^E5ZBq;g^~mHG2# zuu1NM*9=-awn^Gi=gef&!?DgTw=k0yB;Cn!^Tg6-+~o4|NOiO(QaLX2n~KUQ6%lxo zoLP+pqw5+L=2)Y^N};!(m4s>7xRE!??HuVFnN(g}5}i<4UK*{6V5u1?u82&o_z6V> zV|{WIJSnb4x-3T8@TP{mYNyE9$u(FdPQdywGI4Urgh+YWZ=!Cbw5+u zi~46Z!UC0i4^YS(t4DZ|0_lR{84CW1iIRqA+=gCS$s%e5RLiw=OgLz4G+GgdLlPm0{M5H{3vbZsTdHbq3mc zTV{^?JJB4|GY0eA+)0rk2_dKZUc_EalsbBRME4EeZV^$6P@AYBm4L!&5O(DsZW zF`lnVVq;`E2KE`_Y9Dy|+nrA|W7L7<2gOm_hM|h3& zGX&O%t8(2ASduY{ziB?j1Xy4PUxqGe9hJmB}7v{3- zdya~0b7+r>Yf}ZIyk@FyEESDWEh1un#t7W!XU$`G_Pj5yZ9x0JWHBUA8wJ#TnZn^$~R&oOar6WU|q+SKJ^G9#*P6WU{1wsx-<+9+7!51Y?=#!BMa z*#8Owj0QAY|IGbK)!0&Auudx{_h zbW^f)ho^vUO7j+OI}A7OS-(wM?vG!<+QohwYAk%sX{bHKn+jzB5I>}dfneuD%1$5< zKcq$+5Qrbbh@;q{&66>WOAi2mWLlgATQg&t>M%T`l9ARjKmfrqtzCP?f{_4N>2evsHrGhHIrdbsHb3>$uKGu1k+5`v6GudMj1(-{RhS$ zWrlogG@=dSAF#}Ho4GbUr)}gc=3ldg<-`O;WwV%>;-z^&q42thm=#On}(cr-wOY8M94Krcme(Z7oZJVgz7 ztUC?@M8l$lXn;g8#G?TMRI!MnLH1n8GkwNo&V_T#+61+aFl_@SH&IT(ybX9&TMfbF zGrWm@FMPzYtzO1>>)gqaVg6d5O&bBEnZht^8N;k3I}UR~E7-*GagBlPjD_DyjqAoy?uVB~r5`)^H zLA{dkEox8$qx4GV=D4?GbO2HntNd%0vOD^((ykR^v|yF)JRyMMDwcJX7!yK(idHeK zNFG#7C{XLyFJ%uE)+RDhC)Ki4>>@EskRtU_Ez52m2*6a!kXVLc(kPL2oUx3pDqNdL zg%ysuHj#<~W-ZH>^9ylPN^C7f)Lu;m!K;4Ra`s{2tBF)t?wGH#)XcaMbmm3I3 zlQqBEoujD`Et~vlPqVrGHYHMF?PG3Aq@p$BrbH@qG6bM%g-IG_B6RBfJDy=}y4SJP zG+kqu`sSws_(2F>rI0hIwcGp`o?#n*wJit-kyd0_Cs3@R3>9LH^EE`(+gNKkE8r;x zz&6&lP)v>lGYPlrFS5i2WgY7oZ{-io;nx=eqQ$^6ssRyr^(RiE`X@1 zp5?W4e@V8Q2)wr#f7|c2g58w)7IT`o{it(I5plcx87o-fJ-e8hE?ZAxRKAN*4ndUz z{cdKexIqDG83f8-B!I?lmg%{3rF&q&9>xoEr}-_OV;%hw&#_;Pgg~25@6|mlxta0| zb@(3ET+Yjec&+ZCd0D#AfTQ-XF8wvcgSHcX$Qs52h1;S%!!%BA7@I%^TLJaal+G8Yu-bdPF0!xAPm=HkYBl6f3#SHw3KWP;k=zsG(>pJ`s z?OrNO(eMcsrVb$i{QD_0vqg^y40;G;7YYG1J|%-RJ>usm(NB#XO=QL@&pevQ42K?L zW=@=$PQW0@2n5hL#VmrX3IiTYozX1#KX z$JHR+^Dh~5zx@krMl2E;SP{=RbP{=#@i@nugroqH3E2H3J&LVA(nQD%L@SENmSfv} ze_638IY#AUM@(z=*o z&-*vjvet!}sONlQ*-Vr@FPjhlo*Z z0vTe^e9v-p1F`7w%u7K7<*En*9r_SJ;}U7?p?4cmL-5~F$$f)~#Aa3WKPMybN+adP zW25DfvT9_ikew?oFRz>eX{@Z`U&9!wFhsD*+acu@4`w$~R@1f`r_Gbf%Sy^>WR7nF zQg<~XfA+JmA0rM~Mk7rrPFdoi9FKnF9AhHsI39`DnhCH?(lhW_ut^RDkkL0f_l%I#sDl-Sw@O!W4hshHYQVNlKFnc6Cv zooA9!Pt(f~Yk2H}xF7o?d*;T60}^@%KFpKlS+rQ_17>6^b7*V%FmK$%twUBB9B9fs zUN>?L>&g1e3rJ|eGcRxtEqCT|9MZ@p(qd;G$HD3}!GW?*a{q&uS?tCq0}4xs;~u8@ z{z;C-m!zN@4^MKem!6b%z-XV>&0WhpcB3DV*dXx(JLnbQbG6Dg#qP-GxHf?;EHwtx z|8nksx{l52vpnE%z_C2&A9^P&=b5@QX6xu^_QL`%Rv-!_V5p@?vjtEJ(;m ziw4Jb9fv?L5>Ofm2>$K5xq#r`t_KGo__yn^VNs0M>*fN12~ac9%C%lMw>2J%(yG0H zpsJ_#wuPy-XYb@mpZOncVr^nOIg+qss!gWu(xw8T0zg>-AWYq*O$7x1E^R6x_;+bj zZ8G(3uJ-gmU;@-kWa`^Ixuxj07#@q#t>oen5L9n-wW+sh9KXwv)uU(z0u`X9qH+8# zSL<~=1=G76i9J;qOz(1J_WnenZ`1y5ALj?Tk^3x<3@fe}j|_X=<;^UQW$xo{Q?VIG zDe}X9zsnYOb^rZb?U02S9@x*-9$W}e-F~k2;6i{3_j8qzApr^;@SgxvzXOR(G?5)h zWFi+F;A*Qa6v1?W{JDk(i%m1oVgKwFwzlAK(0ZDH4s*2#M;VYD=FMd$43KnqmuuW| z!2rh{@eglh6@88b3^e;32|AT#pCfwI+q9nsM9_3Y&^Vs`H=fks|KWAkHTE|il7rA8 zXML`N1_;#v%KiZa|L2+?5d5F(paBH`=Q?N{a@KJ@_XB|mP&1Lsj_bK4hR33`n*0Gl zbzD#W4h78@oXgxC5U2n(6$Q-~T-_bPQ!stOo5=_R1k)G1O;S<$x`fZc!OodzTzQDA)981oN!V*0SNU0%K89d`bq5sK-70qI{^^&ozzYU zkuhI$l~e%&6QE|Ik@GcAme-47cq~fi$*c+>sJ`aNsz{^Ygi~DQutLHKfSQV&aEhxO z7M_CX6jwPcKro%+$YFJb6T+T-mM8rPC*=Rc32*%uYccXHM}mdw3zHMhX(s@|bU;}j zAe?YcI{^^&ozqSLM1AMT2|k5;n5?T*@Zr}hVl`Fu%OmZ3lu#S4x_z%1BmGhuu~sZBc^b`Ulh25 zs0oxQ*=g{1{d@MZ+cS3>PL|sr`xnTm6LI+o{g9Qi zKKp_yXa&44h%j0K?=#f&;))gUKBGmRdk`*ywctEb_rpGRH;)|&IA{fYB zj8?!$f(Ub9=8?cmhbYNR=?*;n%;383D7y}*-9gLb&s04zSVQaN&x{N?M**Uu&y02* zlrKYqt7CzNsOU4}`rfJ{tfG$wey3G5pst9L??;0M(JK0A&>&hxAB_bq3ZtTY=7;sR&=VCCKr-S37 zfy6a!SY3`>&p-W5AG7H<|1)670K-2623%DGj9Y>Gt$?uNP3a1xe5RW?8v2v}yCW>( z`=7AoH$tG}CKF`S&G@^8(n8ZswQqHKCdj6n*rB~A$vm8xn*O9u+50!n3OGD)%nCR> zaLh72J)3yom}PeC?EYDDWZ}%w^rs$SZ{0K};K%~UoPZ+>9COV0J1z?xb4<8}E|9xC z8@qZ_9Sz(9h@k+io0N_DX`ZQ$2JjSA^GtO#00^piMD>uM%E9j5)XcA1%U(v&B4G!~bc zxMBT~g+?WdCBeG7Eyvi$eAGJK7>HruI#a#B0im_d%#f>JrDS6DOCt;rBW#_iUVeZW zVe3r1{Jy4MOr}-F2LIG?c52jypaNQCYzQi#MaBj*LmntY1>V|Vc9(~PfT&=Dd2OMp zkD-DM=1u+G)3O4zc8h=A7wk-IOHcv5Kp(Tq7Kx49CB2@+>ijCzIn}H~t&fbj&O$66-jE;&GqB zVyGumhacBN-GBTPvkL%qMYO6pu17k`qN3w^qyvKKIE{2VzfPtNTZ6yv6dPL55HQfK zh6X*pQ3ebRdVB+dp@GIXowU*W?<-TBv<*(CleVw)7zctLpgd^9k{Vb*yQyFH33 zKzQ)H*;KBR0TJit%{KBTO$sejzccv^%aEnTf!=o}68SB#3dLix;DTwU=lVHkS@V|o zQ(k!ykI;?_CQgMH8>%o2xl(u6S^Q|>-YeL+i?uD4q&4dm3LrXeAp!7oOP!(#3{42+ znV1lW5?H++kdDCfM=jnocc)P|;5@sH^`PsXlnHIBuz^!kWOUks*S=v7sgyW*TGAx|&eNVx)7AY&^<+~;`IQj;a1urEVqxB<}x zk6P%0Wi;caP;(bsyhrY1R;2FOcWe=l0ct7=y~UP#$D%B#7F)Q$ChMhr(_#xrF9p$? zi>>Bzn}fpW&Ba!`Zg4E>!BDdCqqXPay-8;ImXj(@c<`8db1ByTx&C(-S?iYgQyy1| zM=%mURm;Q{jQU6%%{*=S1>ds{EuT&-NS*m~VnN^)mj5UU=B-F9NX=eB1%FHxUO-Fg z>kfX;m@}$AXg*C~^?@g8I;yu+oYyQ-UvD9|N%x`QXY|po zMP2!;qjm-SG~Mh9_-VS?rGK#q4H&h{!V*UE({!`TQV9S&1^+G!8}t*BA5`!A1&p5@ z`F>CVWdPm}DxhiRef?Xd6fw=bZ>hHqAS!s@Qg0nVRPerqw+{V;FNGpuuRophzN7XA z{Iql08}QR4v^Q`NO+tIE#&W_31Niq^x$-p)2>!iR>kcyWlS1LL-*0B{rN7!AR6yIR z{Xqq^t=ey8=!m0j)qV?GGwC7PR_(X)NGdi{l(&WEOy*-8ml&mYf^u=%w}q4lvko4kY~am z;q-F&yM=Q$=>=STLBcd>6R;HMCP4s=FQ`dc1GvF5Td6u?hVTL;E#kG37aHhyx>^No z`7+Tg8if!9X5pef1PNxrRVRYZF+^ew0^K+VfIy`3XK#90u@}OhaesS z291Z4iEXFYhahM>U7a|Y*mi{D*NH84@LLp;=(saDuGT|<0jb-L82Bc!?X=s6AZR;X zo%klP?N~>hvdpG&chZ^<0(r5MI+5-xoTB+3Q5Hj(LE((W<^CE7z5~=@Mn`F9^p7p^ z6l7;Cl`scH*POAi@f0&W9kQIW_&@XvZ_vB{oP~?8qBZh#<-BF)N!tkDg4!UFR}oW4 zg8I%|smh5Y0LOVNM+GGbQ0I9I$=-Z=6+nOj-}zg@yjASGL?&9$e`l%ta-tj+w6fIz zBtw<-Q;Bxd)j^cuq(fJy&@=Kv4 z#3lS-UEKe=84z5+nkyMxmn^l(!&7iwveYII5L}lmwe(Bj$TwW}Z%XC2`(3c8E4Uoc z;3~Lz*;4yE6a>v>OYQFfL37zs>p#j*{Ah{wU-uMR|NUsGJsuGB0Ok4*5Yh3Yh4o)E ztQ%7h8#8SG`()m{^9);UGE&4QBN0MHz+OsartPEEHx$ma)rK%dY&8-gR0O0|W~yh; zvXkbkRVL*&X4zO}(tbIWC;5XsUeFJJz=^YM_n99ub%@-OrJzD@av!KviGdA9N&5Yzyr z{{YdldA3?Vr+M}hcG7y)GRnC;VIyHa21cdPJ7=NoPfFv(H!jphL5i9d+G_0v2%Uwt z`f)fQj9O^pQotAVMoXiHEwlafblxy>S-?S|zbxROhAp$zRueklSZ1r2J|L!;Wwv_h z17ea|X5*z#TdXwn|I@a=<0Na^D~)!VPwOB60&5~fy9IoQ-Z*JAiLbVKQSMgm=X}i? ztjO@9{zd-sOn&bu$mLK#DOkEXu$0=f+E&{wJcXsJ?Orku4+u+F+r0;i>4czoTyTeo zlr$<_>lbmWo;o3#4?p|eSMG$4%Hq&pfA?%Sk08W0`5i8`9bSvq-s3wf_s zKm3iRZwYFlak54G5KpP7_8}l@*+M>CFAYVLw)yYn@K;7{3n-|cw*`h$ZQE?Mic1&$ zyv@dxPO~W>4Bcku$!!E64BcjT>L$i=I(2is>?FTMV{Z4Z4{D*#uGf8plBlI#_Yt7b zR0En)nQw_c8j?WLwFz#LLML_7jtGx&amQ~qWd=FuVBPd4{1Z0nV8B5G`=E9s3Q;ND z-+TOBZCjo=0Ky69?aWqbB2Y7_;tTOW1w>82x+d!X3p!Bo6g6GY zfeMJ4E>NIS7-bT=q(@^O;^(Cu(rCP_g9!*~ zfHIf>Vf$qYrpFAd96YQXoTQ~r-8Xso!BzGw2ltuYr}vLXYthFXKHsT(v?U+NVvjkl zyz7c5)YWqxQ~pj#U^oB*xe65m*fZBrYeyk~6>}Z+TMHq8`Ewmy6riOo2~c33W407a zTYGP9x}yVxmH}!i z%DF6ZR00uCLAA)qlxJOlLKk;*q)|{^?5HbZo_J>iY9`9AEDnsKclKf@rcP#18dQq| zccZZHaCssIkAWFbQ&Aqp4~(MSq3>kszM*ujPthQaLf0&D)a@IO(~r>rHB%o-nJfvc zqHW_6$CD`xlm^uj2iI>tp#abF?A1=vKOL6)Pwd;Vf~xD)j(^BvtC8sto31RH(Oz)) z8b>Bcfx-8J9he}qXA3;95$|X z)ZUbE7W}p@?l%&EalK>GaE-xENU~AnPmWnA2CxvI$SXe)pbf7&CO*YNZJ-qBYYyKM z?<<l7XncNxpV1xSiRNZ+lA-JdCWxG0ut?cdNpm;QxP76ZY#}|G!+5D z^IK_bNYCTd1h)L`anFn2PQ4y@o(a#tAstPAh5(HJl_-dj=eLnxY3igRbW-}JWA;^! zMy79v!*|6UOX|Kho1ho?q z?KTLY@|GjqCMyBn84L=U@Pj}P3YqZRDTfh-IIQ07@I7%y(~B7b?Pq#1?{?@4U`%+H z8nxTW>!}<~v+8cAcY$&=R?%SVvnTFoP9gqw;Ak!!{Z0_#5PLx2+R zJF$2~8=k$_;rru`rcu1tadMQ~L{9Mo$BZaP%bX$vGC!52a*7`~*wRWzi=5(zK|sr# zA_O|1DW{0|`!I-Tu7N<5h(|Q6KH%`faYxe|2mRI(>a z%uELy^~+;G`rX0-2e*`EL?i7Fwl0U`jyA~A2LngrjvT`EFM*>WfPsGv9Bl|kAENk` z0Sy64d=$(a_|4lV4nL~=jQFFM*(Z)lVFS`M|7pBV!YBZNOq_`V@WH1Jc3INV;s+N; zf*BYX8W89i7y@Vwqs6Hc|HJ$fIjBn7PIs--N-%Wbo~N8h>0fPz!i@ETfEW0 zM?(P04X4I`pUs=I$a6y$zV)a!mSX-9gwPN$qBOqu`}W}ZoxTq=DA*Gr zGz4s^G=A_O@4;KgesEId=~b5KS3q4Qb;%D-&Q1{?Rl{~ z(xG~BSp_^g0Uw9LFYgA>7sMjt@$n4x1u}e5u9G}-*KCnu`mx-htNHU$?_ z)Wd0T;=>^vgwQ+!NR!pUc#6KruU{j#q>Llf&$gdP`)B zeJj4%5i)7Ea6~1Pn1CQ4rS;zj*=e+AlbLfuyqj!= zSjPjZMu~O&oRI#B5DJ5APN-g>C{lWPS)==@H!|+%Z3tC3lvv7%i9|!nXQh*#gWX3#bJT$QH=Y6RL=h z{qM|=Uo3GW@;B1|gYo#(SQ-87IT#<>Y+7!@k~l;wx-JNsL9Sj9G=r9#3#b_%Gt7TE zp1mxTv^?zpy$3&-O?M5Kg^(DXnTbxyp$^|&x93_uEmR7CYIf^t08!2EkV<3#LUng2 zTORNOqL$q<96?0W#M>dhEG##>z$pE82&tV%9Si|rs_JOn`ab;kMod6dc2oze5a9iB zG^BpXB?O>28p@K?OZEH=*v8zPt|;R?v{aV>x6*aahriXax zO5`j}B&C(&(r`8{!DS!OT5)N(Yd5-r$<0Qd{aiR{N0{XvW#V^B{)4yiquGt1^;{U= zq<#QV*@!}ARTy8>=G8F()VV6Gf*%l3xhkwyP=L@}6>cO?C;(C0sxam=T8=fMs9YW9 z^(rcXQF?XQ&2}GWa2qgHRU1Zl#3BPrqZKt}nDwj0YXl!aoQU7Qj~>J;Vxl0-KuOtj zAwbh>!zx~d0OYk{6|X`7V`{@HUWEXWQX5wBDg_9P+At!eNZA99HDR+=?oA2y_=E1? zz3!m$6s~w27(sc$HQN88gdoV4mo|g|Myv_r(guC9f&{2>ec0cB2cI-pKv1qvq@>te zpV%&nz4c-Bi%Ova%JpI7Fh9{=gkWRXe_}8{SVRRV_BJMRQtWL^*!xgGbwvk7dGGYHx}{YW+)-jXLVtG8+rf+yAV+4Z4LWfhwwh#lmN6_!?x>Yx?x=g z?cEy22D2cphC*+I{bz^p9$lpxjqx|ab~r(ec35wOu@mSSSA%4m&xZ1@6c3bHgVwgN zot&UW&fONqMO_*|C`AK%dsrne1eSuJ18JkRJ)A1@NTLK-w})|FP6J2?;F;}V=2T?dG{zZk~iKY$5D>KtYxSMF<+fpcvZ-c}8GA7CwJHyQ$}64sYUT`wj_F<}s&f4-GAXy-WO?gbF@B5x!8#287j> ztaZ;P9GKC!DU`lpYW4#iV!hwTq1khst83!lAO5RLJl_fxE=XSfLCb zgpx~~SPTD3STTqUUmA`Y&4%_|>hPGT5kiz%>SRiL0KvA@3G)3RLIod}hp&ugSM^=4 z4H(49=H*VJjbdO>S?;te_6Na$!GXKd$#~Xbg+0PQ-N>4TSKq+e=B{*No$%SV2a^%2 z!x=ZQ(w?gw9ta8u!MNJVZ>b>|S37Om`ei}|AD;y(*-n6hoxMR3g z5Z-p?*j?ap}J@(U_Fe zN`^lm7*Z7e6@mecc{wbuVk578SqEMjr^dYOWa>((F)vf4)R-ZG`+Y`PvHayoJr>-`S}zZ(-Fu z(cJf{Cr_O{b$U(n>>}1BGnKrRrD7d#yt!gb`S1h}0zYTe(5uTwO&C*tL;27u$gJYq z{esao)zg0xzERCOq>|O_k3M@M)piE^f)`)q=afsn;nhEx^z&)6gR~l$sG(vOYn_=` z;rsrG>PfSIKC}9!>e)d}_{;m))xBE%jt7lc$++H1w3+?;afR4?Djw^Jsl#V_6hVsfWvFe(i-&J+@jOt*1IPHE` z^@GAR`W#i%y$h3G?`gF1NKx;9zm6q*W5mR^3<9s!S9&gY}V|V`0oT!`-ERi znl^RvB;hd|?aAreCw$`Fqsl9+2u1T|l;eOW4Nxz)%6WJZJnxFK-RTEje;A$vm zjEUo$>Zw`b&ywuRi4*)L|Ifw-?ftelRgI~ryk^3Pitzz{?Ybk~(!;rAb!iN{J!2`-LLbI?ULFPah&(nwj z1x+=wfLav#-7ugS2BI}2{B9BJFj(K!(rBlydtG-LwiK~j(&fkje-?nZpa2CD2JoqrZXVy0%?30?BpVdnghw zutxG;N%hSG%gBhu1Ix&GJN*qTBa6Fg1eTFKdMcJ!V2$Fv)TbCoMnxnskc^7c5SQV-JP_K&X@bRHuRY>^?ndC#gs%I~MtyGT(*4XgPr`WTlVwA? z#RMzW<4_;lvbBG$s5~pM#`Cs?gTr-8S(}MHihg;=)Z6bUnmVg!QccnH8IykY^Xeiw z_4LxCBn#~vAJvqFn#M;pWud0=QPgLlrtz)X_+2mxdEiXS_)VC(jCDy)iZv76z-biT z##;)*06=`3A_fBSZHgEO#J8!z2n6EWFc>MUXjQZ(bLkuakW7w9u;yBmRbS#8l`QHg zV+Rl{le=_PEEts4Dk3=NV5yE+C~&JI7OJmWMFdKMq*{fQ08@B()gB8ZQz8-zBvZsN zNjfl^#^{umope#KOhG~_8w9;O)Hs190!ilsf#nW0PJlpihZ-k9Ai1M^4^YA@7S{Zq z^68eWz#{+s)Nk&$^j$lNkL39~?bgLvMHMx-Py6|-slTX32j}=6X^Nh>)6Oy&eQ{?t z)_DCLYjCw+XLX14@+`LF&rIZL8N)6&>!;+&SoPTe#uziGry{WJ}| zoekTG9X(ZJ_l`bOU)7}1QE$~`7n|>(t7+fO;3W8JFn*-7QTdFh-yLwxh`Nkswi#*= za!xbbj6yjR0D)vi)bA)bllNAgNi*Bbh=gXgnNje#AelU~NJpy+mYI<$nA&D@9TGIN z&5l@TW}6+E;(=v$p`7P9_dqhcLq~r-%?A`%_TBux@U^Ge$|Q|=NKpK4znSVDvG(2b z|LJ>tM7{S!?v#t-JyGufgY2HD?}1VKJyG9_rSV?f_sRc?EPMs~y=m;!_xGl;ld1Pc zeUI{Bzc(7`n6>Zc6yP+1Xx6?zauChh_s8WNhi{;{zeQ)cC;@`yek?nV=oSbHS{nhF9$`i8{8da*VaM`#IB^YIQ~r2574jPU@pLLo z4iFSHkEm!H^Eg8Uy|1Yt2*YciVGZSBIu()}J7n2;ktTUQWI>@3kS1&0#(zjt*@3+* z9KV+Re%P{fDr7wNvUDoS5|^b@p_3s1RR`)ZD+4jJBJ8n_b?UoQvg2* z!JjDBENbnVaNasrck3Dx4zv|q!;l@wa3JrlVajh8V%l<%kY+KVTl= zF2luE-=b~rIu;JOzy_29puOu%Jz@vX;_}1=Us2CG)>KC!Ier~$*WSO1EVU7KwTwR> zCZ1zg=G8K|EJ)5~J!33=FbF`uk=ZJ02up$T773uS zk>v&cQt2ER@FL^og%7jv+viyK>@K(U?t8_6D+XM4MZf-C!!EV#$G70S9HAD+3X0Iab{INKlqCnajepJhD?X{IPkR4 z0ay;TZYyij99w8wgWZ=|#)@$2de*n(Wfp5ChN?|Idxh~vHB^DY@e1?eIE->_KpLvs z!rklH%~jj9>x3BO+qCOQ0KM0>>jahp={g~R#_Qy|Un*wc9pTW8tRmdDft6J4V74ht zL9v4hQ_qk#e6y3;1)}Q&20a9_FNFXaJINqTk3iZ*^miwBr86Usvv;L4!;`IZ2Zs`Dzy_G)7?Pd8*{PwC` zJh1nL6JKB*ll#*7;wZO|_;g=p1@``Q{~_llfoWP6H1@N=csx6>52OcIHZ&kG?#qV8 z0hUnlawnXT6W9lt-7IQm4pbnJb1!)-2Py|y0cB~@eYt^sC|x5L8W0$bTxc93jm}CV z5!inVr*CFmi~p8xK6d!_-^hGj4)fh%D%bJ4Ja8UPFPn$5hhs z8o(c=YY+tlMk7h%^dnNCl-(xlPB3hTXa)nq5CoL7A0S5B37ww+g6sstjz|^82tUD) zC`l%K{Fre$Q`(et5!6hyx_`{D-%mQ>4 zU8b`CWQhXBZVCNn&HWS?bTH)%OI2=R16bJ^Lqco&8RIBg+0U>*dzIGpGYludzZM)Q zdzOW7Ze__KXAOlT#3~oGDf34uv&Ct6jF-koYiXIgh#+CmVnXO@gBhK&08lSacm+ zjPQ&#p|_p2ORnTtbje7EhRG_fh6xai1e8Vsf`656E+F_ zh6$i%q9kp#Zf-|>7Nu2t0YSBz+S?hXx`DfnXS@;a+0Hs8*KwROlBq74`m8n;2&Mwc z3IJj1v)WWZ@IR|f1qA=I+EkZDRxQUql|m2*On{n+Os(bd_Tp<2pGD~oGBpAORV_zq zB#pv|-oSBeC8=mc18OQ7(Hpo*OPZs0h%QiZ{^f#c}v1qywawkma;|Cw8bPxGQN zlWL|^7flb(?qnUpf*tJJWF5zzvlZrElq6ql;s0yfNqZIX6=vYG+|399yt9R?-HZ^R zQCm26o{|`5DUo|eHS?g7<@=}xm$*a6|cjYpgwO-}Ddi&EvM8Jma;d^hgnzHSN zfu^kOCK71M+RoM9)200;AR?^~BFzijH+e>V_^Y>AN%Bn|lOxw7@4lrY4G7f$%1#9Y z|67_L5d3fHNCO1_TRPG_inLu^tsOvM0@O_8vRzzF_(^;grPbO22&!FNtsNe%9q(}7 zMR)=TRDha_BJCZn?!)jEOz-ekGAseX^bYUT8T&78#{zdZ&uHMRuxZio>RGqXoO)X| zPAJ25yI6j5H;>W5s%^)}3462?fKVTxtPc>T@6k>GM16a-697@)9_@q}8S@^;Q3HiN z5SRcp6AicbcwAngCh=L6Zr53t2Gx5U#|_dbIAJfxv5ur7Cje?Ha>8DYgB|r1OnW&_ zb`%8DUXG)k5;!33BHkU3Sg*78(xf^o53lI(?xP$e@M}I2Kwszg}kv4)pqx)OY*Yb8PGq!$IA> z#8OMYF9!S)DcG;7MSod3(8G72) zFEjKM0?Vv~JP+_i2rRQo`YJv;9$02wHe769@RjnQW!B}x{7h5 z6%-?DOcgXwud&oi6m$$Y)>vKT#4lzoz#e6)M$FuRSCsmrs0DO#u+HKe!cm9W$h>uy zm+ue9Ite-T;j`g$huMhYXDvHZEJZ>Lr#@?CW&78ta;UsEOf<5J;#y1X<)|EOLuxH- zg>^aL_2IoJmt1eDT{e}Y$$7nneK`$OpZaE_#Rn^6aSl#UjxKz}=-Ozh%nuM%Z?u}r z!2pPMZ?rn=o}<{^XnKxfcO&)O317MvTy?4IKVXwtS)HkZvgJAxV3aM_S!!1CMYddL zwQc7gfQw))IJc+1IKqCwliLjkWy{-5_fWRH-2@nA%iB$Wc`$RkG1DVTGE=$(AK$RJ z?mHYc6V&dYl==--PZHKpPW^_JkW&*NDtg1}(oOj?Cb+tnYKV&7u=)>H6(Qr^Vf;=R zH=wSF&ZKvk22sYn!!(F8?j1?fqD)k@!?Y+9740xBLOQ zrgA|wA1Q0$UjS~hR<=3Q6K*8Blh*!dQ$^#B<*@r z1Fj?i)#DSWpoV&@wcG{(qK0~_sE=}492L}C-AnwW;Ku>Tp0Ly3*>A?}G5olPwD%Z( zIuzMssp;G2xQPVBH8xG(fZ*R_b&}IJAo%xKefs&2%Ld@Qq#@)d*rL(~Q$aQ=XfOdv zXDAI8UNT6=Y*f%dT|v)xd^$eaAMQKBMwab23^`!fZy4xEWxo~3wGk>{*l*#UUhFD; zj`Nhp)Yy+%J}YfB9J%0VG#qru(rD)LTyQj6IMJoetoeKFS<7(Er|kDbAk~Zk7*86;0E{QCK+i1!7*AT=d-^*iV}4+N zZiRQ9WN(&zZaDJ6@wws12gm2eN%`RToSZ~Y<$PX%^8ibnpI-xjVR6O~6=1G9W2y6V zdZHZ5LR-u-#{D_)rLR!83$nuwS{LV(COn$j zc#3_%$32S8y&!}bmOg5$H60LIkJ<^D2`eQVnJ^6!Kn%M_ZM7l-V%R-uV@3QIb!MD0 zk453%PqWkG7MTht@mORkpu}U5o#?F?De+ij_mvlEU_b?n?90pbS5&abzH+$#g{%Or zT^e>h!@f)|H5Jgxx71WXRxGvE#1!Y0l`OS!)+1X$S;qjo zbcIi0ex1$R6h7t_RgSJIA5=7adQr{vRI{&G!WxV(x*sWdsxw}oVC0)aN&$B7x@Ljip^0S?$6~w*MPRMBejD=fRft}NSSDxgGYm#roQltBf%Y&9VOf?=19 z34v~c@VMOI8DDce0x(L~n}Bmh2PtNSQ{p$UP!T@nIO0(tKTl#XW6c>m1iEem(^RL^hO zb?iDBO!$Bv!Ma-jGmCoVV3h z45dJF-p0!;=}?+%&f7T9QV{+TwE50RoFGR$A9I0kN5=hzOk`K=qkJ>E;laOA&zOgwZD{JklcGIy&&-ScK8_z zw);N4AT{`VD)>F#kD~o($;#B_7H8hLmHG)Zr10TNN39lsFnOh;&i4UH$-())Y(8z# zRyr-ZX^7^pba1Bm0!`*wG&8IX7yA59<+41vCMy zb<`#vUs1tY2b=7F$_hZWAzU8kr^aqD70@nzgQ2(Qz(~L zK=5yJI(Cz%H(3-fFNXJJ^QV6NqN#xPNH3ZSXpi)wlh7eYd!!c~?6ahYXpi)wQ!JNP z5E7%)v#-BPcnEjFF5b7l8qUh$ZzWTXCojvgaA$)}>MN?>=CstILTke|2RoqW#iW5ia^H0L`;Jw(oEKHyf#;H`ehJ== zCEs+sW~xo%DbQPv-AYYX^4Sjr@>VsA-T}eDw;UXo%GnFU1q+C26R;HMCP4s=UDPD4 z0sOXOcTjaCl?A>$~8YvJM75owef+zwk8V`R-Z#%_41g7ouF5oZeZAUonPH(Bll0u^6 z&f*?acGG~=ZAT33NpCxC-61e-r*{E+(%X(~=RL=68M%{kH3;M#0d*o>_`m01>PM6% z5oSGZ+r#U zAqOv<<>;dWheJ+I&)y;z%c2v7Mn~+?h5>-7(Reftrbb7lWcUiEMn|P&fM9BLR7#e` zaddFh5h+>thbG(|eAMt{f#;~BGBT6|&rwHZWPso~>Zpv2&Kf=n_cr0T_Wa0ExmcFS z#QC1NRdr;|rDX(Fw&T4l+L4=VsHCeA40V!>cZ^{=)#_MPS(aE&T00X6dVtcIfN17vawau1Cvd-XGXCJojVc|ZeCgl_d?IYip=O?Q!iMI2(vWl7 zDo9b)IlbxuLg$?RRTvDY^gg9gAw!$B>(sF!}|fa9WG z`T;S~T+~ZHAf~E|wDi+$fCp*G$)Q<%p36rUzRknT zui2+TQPt?-qv^q%*$#ctlHc+p6q20^6<9paRp}NWES~48?GPX=p63pfC+vW*c%D1B zQcNxc#plxNMfl`!D{R+{mkz^!(BBW5I_To%L01hwd_^4(x@!0Vg5*J01x^m-Y!A6| zM+6{HWGwE`@llOGd!)x6>dAZr2vG9E8HG^#JJ8CNxSME z0CdkvQw{a=O5Hv9ifUHs?g50QD|PqK8RZ(;J(amMuGi@P0fHW&>>ofhX^rlm1g=Ef zj8|3v(81(77Y9(}z65nott*y*aA7fb%WAc0_yp5xwG#oMSgV}~2-9j^tN@L)@lB9_ zwxsqK^CN8B7Q;b(&FJ_4U;H zFDXd#Xy5;y%l{fq@50Xvd(TzrFyvJC`>sld1x7Ux$lOZ^(2nM16bqpoOh#WPrHJ9f6TkKzk;_2;f#ED}?JVL1dcFBJk9^tr3@QXzm9pS$W820{S) zKX-BcPsuI`P~fy{w~sif`yf!9Nh$%-)6^1rz9UqG4z9W*0Z32Nj7lkLKxUqCxqNT{ z1VTU=4}fs*88@CUrcOX`opJHI1)50#!F0yOS&ewx7|1t*ZrG)SuSlLXBxKTAR~`5S zBB;)~>b3vw zSG~LeLdyU(6&=HT?W$8gdUmea77N6p4XByuyyd(xiWcqj zZc@E1Kxt5&H||E^3oe%zUho*00W}q!tXwcg(SGrQo2UDRj#e*FG)SY+HUDqJ ztpaMM>*x^W-^ME1NdDUm&igX-Td9@YGl0z5x(=Xn{2JXZKSD@v)ZxV(SAetjFtD(yY<(G2%tk1zCW#5;bl2m!ha66s_>biu=(dT;{>x`(|M^5O>&)jjOt z;wKk_1rQIo9`<@Hgx6>^lK>ZszxV8RVq_A|hu?UC+Bn2q)sArd{ zp1^eexW}K2dV(hG$2~7sIY~VBe!@6jzVU`Y9zo>O2`QTVgr|-mNC1XKo;oKLSPGKz zyje(~vdB~C%_IO`?5W510z(A?d6q2%q6E&erC(un=<(&kujCq<#URjbr9~+8)TD#2 zU<@KqF2!jy8b1jQ@NUgPoSBEQn23xW14ExKzw^<)pF z!M4V0H$b_Srpqg%HSte$pVn zZuHdeVn_h|e0l+L1Oz(3Q9zW40^B0kBsZn2kl_#*6*?~5B*z5x1RA>8<6EPipv4CQ z-4m3xY&JbXY@1C_(BiY%^aSnzK$a}qs=ER&wa^oPGChIEZ;0O)O;11o$~w~%mgtEs zo;^hM1b)nw^7yM!PtdBB^3>^dK3=Ya@TI5*;3P#jB=RIzlt7bT^02Lv5ssf&;BeYZ zufS5Er&kD|@v@v=DZ&xa$k|Qb0Yd`<-FFo7B%r>dXD|5(X!kXb?}+?L`)3HWUupmR znx_u1Q5tlwc`fDL03iJOnuk}&a^j#DwXb<4J0iE* zD=rgJ6r;&dgzZ+q&zmI@FD1oFIA2t)~-*UAYQR@Zy{{m8A9 zmO!B0N;kgAdQTm2<`DyT)%A^abu(!eM zDBsfnVjI)ob-h;3Ab@0DgV*gEaZp9?X!hX*mK^l5<>gks=zBo9EdhkaL9a;*AAggK+XaDp z%**&N7PcS84;0Y5oMRr23Ld~;&an$9&K9KN!}+gclL6qK1u^9wK)7c?Of9g0P+br! zkoRAJaL-FJz$i6D29E`0uO5;FjciWb$S$^Yb6ClWvgRql@bDY zV|7gZia`iKu{xG7V_pbQp7tn0fIe9rYt`LP;CD1qfVQuWweIHcHJ5pzE;AKo$K{*9J%e9giXctnOZUuEmZMqedSk$IlK^xrKbSoI$ zjMS19YqOLU5PT4`yB03OUy;D?xx_WthA}+h3M*&@#%E&%?O;BLsXZT=MMLw0Sb@B} z5&~H9K@7VX8q6dBJ`%ILMZTf61OmfJTbU#2oV1=CNyZ9jL`r|sl5!-5zw@CC^DJw8lb&A3xSyVB(x@PuN{6(r+=%4DM>S@(?Ro8@1 zkK;8-Q4nUJq-?qnpy{V{qzD1XPw7Yz0vK~jM~V<2Qcmeekpcw9DTj@caw zuS~NieE3#A@OmmQHuC94h<&{FzbGLH3gn%p5WtAjG2Cg=&y`7l8o!E#dE@zvN&(TX zuhJM1fabX zbA3P0&(vkm-b*p$%cYST3Vjy~k4@m0l}I%j Date: Wed, 27 Mar 2024 15:35:44 -0700 Subject: [PATCH 038/168] chore: release version 0.70.3 --- Cargo.lock | 88 +++++++++++++++--------------- Cargo.toml | 2 +- docs/guide/src/penumbra_version.md | 2 +- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e06cf59c38..b6c4ae8eb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1378,7 +1378,7 @@ dependencies = [ [[package]] name = "cnidarium" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "async-trait", @@ -1413,7 +1413,7 @@ dependencies = [ [[package]] name = "cnidarium-component" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "async-trait", @@ -1892,7 +1892,7 @@ dependencies = [ [[package]] name = "decaf377-fmd" -version = "0.70.2" +version = "0.70.3" dependencies = [ "ark-ff", "ark-serialize", @@ -1907,7 +1907,7 @@ dependencies = [ [[package]] name = "decaf377-frost" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ff", @@ -1922,7 +1922,7 @@ dependencies = [ [[package]] name = "decaf377-ka" -version = "0.70.2" +version = "0.70.3" dependencies = [ "ark-ff", "decaf377 0.5.0", @@ -4397,7 +4397,7 @@ dependencies = [ [[package]] name = "pcli" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ff", @@ -4475,7 +4475,7 @@ dependencies = [ [[package]] name = "pclientd" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "assert_cmd", @@ -4527,7 +4527,7 @@ dependencies = [ [[package]] name = "pd" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ff", @@ -4666,7 +4666,7 @@ dependencies = [ [[package]] name = "penumbra-app" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ff", @@ -4746,7 +4746,7 @@ dependencies = [ [[package]] name = "penumbra-asset" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ff", @@ -4785,7 +4785,7 @@ dependencies = [ [[package]] name = "penumbra-auto-https" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "axum-server", @@ -4797,7 +4797,7 @@ dependencies = [ [[package]] name = "penumbra-bench" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ec", @@ -4841,7 +4841,7 @@ dependencies = [ [[package]] name = "penumbra-community-pool" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ff", @@ -4873,7 +4873,7 @@ dependencies = [ [[package]] name = "penumbra-compact-block" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ff", @@ -4909,7 +4909,7 @@ dependencies = [ [[package]] name = "penumbra-custody" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ff", @@ -4944,7 +4944,7 @@ dependencies = [ [[package]] name = "penumbra-dex" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ff", @@ -5004,7 +5004,7 @@ dependencies = [ [[package]] name = "penumbra-distributions" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "async-trait", @@ -5021,7 +5021,7 @@ dependencies = [ [[package]] name = "penumbra-eddy" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ff", @@ -5039,7 +5039,7 @@ dependencies = [ [[package]] name = "penumbra-fee" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ff", @@ -5064,7 +5064,7 @@ dependencies = [ [[package]] name = "penumbra-funding" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "async-trait", @@ -5086,7 +5086,7 @@ dependencies = [ [[package]] name = "penumbra-genesis" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "penumbra-community-pool", @@ -5105,7 +5105,7 @@ dependencies = [ [[package]] name = "penumbra-governance" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ff", @@ -5162,7 +5162,7 @@ dependencies = [ [[package]] name = "penumbra-ibc" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ff", @@ -5197,7 +5197,7 @@ dependencies = [ [[package]] name = "penumbra-keys" -version = "0.70.2" +version = "0.70.3" dependencies = [ "aes 0.8.4", "anyhow", @@ -5243,7 +5243,7 @@ dependencies = [ [[package]] name = "penumbra-measure" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "bytesize", @@ -5262,7 +5262,7 @@ dependencies = [ [[package]] name = "penumbra-mock-client" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "cnidarium", @@ -5278,7 +5278,7 @@ dependencies = [ [[package]] name = "penumbra-mock-consensus" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "bytes", @@ -5293,7 +5293,7 @@ dependencies = [ [[package]] name = "penumbra-num" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ff", @@ -5330,7 +5330,7 @@ dependencies = [ [[package]] name = "penumbra-proof-params" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ec", @@ -5359,7 +5359,7 @@ dependencies = [ [[package]] name = "penumbra-proof-setup" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ec", @@ -5387,7 +5387,7 @@ dependencies = [ [[package]] name = "penumbra-proto" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "async-trait", @@ -5417,7 +5417,7 @@ dependencies = [ [[package]] name = "penumbra-sct" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ff", @@ -5450,7 +5450,7 @@ dependencies = [ [[package]] name = "penumbra-shielded-pool" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ff", @@ -5500,7 +5500,7 @@ dependencies = [ [[package]] name = "penumbra-stake" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ff", @@ -5553,7 +5553,7 @@ dependencies = [ [[package]] name = "penumbra-tct" -version = "0.70.2" +version = "0.70.3" dependencies = [ "ark-ed-on-bls12-377", "ark-ff", @@ -5584,7 +5584,7 @@ dependencies = [ [[package]] name = "penumbra-tct-property-test" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "futures", @@ -5596,7 +5596,7 @@ dependencies = [ [[package]] name = "penumbra-tct-visualize" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "axum", @@ -5626,7 +5626,7 @@ dependencies = [ [[package]] name = "penumbra-tendermint-proxy" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "chrono", @@ -5657,7 +5657,7 @@ dependencies = [ [[package]] name = "penumbra-tower-trace" -version = "0.70.2" +version = "0.70.3" dependencies = [ "futures", "hex", @@ -5678,7 +5678,7 @@ dependencies = [ [[package]] name = "penumbra-transaction" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-ff", @@ -5730,7 +5730,7 @@ dependencies = [ [[package]] name = "penumbra-txhash" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "blake2b_simd 1.0.2", @@ -5742,7 +5742,7 @@ dependencies = [ [[package]] name = "penumbra-view" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-std", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "penumbra-wallet" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-std", @@ -7609,7 +7609,7 @@ checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" [[package]] name = "summonerd" -version = "0.70.2" +version = "0.70.3" dependencies = [ "anyhow", "ark-groth16", diff --git a/Cargo.toml b/Cargo.toml index 4e6a99dc1b..bce111bc81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,7 +98,7 @@ push = false [workspace.package] authors = ["Penumbra Labs "] edition = "2021" -version = "0.70.2" +version = "0.70.3" repository = "https://github.com/penumbra-zone/penumbra" homepage = "https://penumbra.zone" license = "MIT OR Apache-2.0" diff --git a/docs/guide/src/penumbra_version.md b/docs/guide/src/penumbra_version.md index 56636a4aa1..7c8613683e 100644 --- a/docs/guide/src/penumbra_version.md +++ b/docs/guide/src/penumbra_version.md @@ -1 +1 @@ -v0.70.2 +v0.70.3 From e44f91e5e2d13c79949f4be06ccb5081de4a0297 Mon Sep 17 00:00:00 2001 From: Conor Schaefer Date: Thu, 28 Mar 2024 08:10:27 -0700 Subject: [PATCH 039/168] chore: fix pr template rendering The docs at [0] say that the subdir for PR templates should only be used if we've got multiple templates, which we don't (yet). Moving one level up to try to wake it up. [0] https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository --- .github/{PULL_REQUEST_TEMPLATE => }/pull_request_template.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{PULL_REQUEST_TEMPLATE => }/pull_request_template.md (100%) diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/pull_request_template.md similarity index 100% rename from .github/PULL_REQUEST_TEMPLATE/pull_request_template.md rename to .github/pull_request_template.md From 9a816775b92937fc08216bd6a0d337b961978a1a Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Tue, 26 Mar 2024 13:19:48 -0400 Subject: [PATCH 040/168] dex: move `SwapExecution`s to NV storage --- crates/core/component/dex/src/component/dex.rs | 6 ++++-- crates/proto/src/state/write.rs | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/crates/core/component/dex/src/component/dex.rs b/crates/core/component/dex/src/component/dex.rs index 788c3c52fa..d368ba0a32 100644 --- a/crates/core/component/dex/src/component/dex.rs +++ b/crates/core/component/dex/src/component/dex.rs @@ -241,8 +241,10 @@ pub trait StateWriteExt: StateWrite + StateReadExt { // Store the swap executions for both directions in the state as well. if let Some(swap_execution) = swap_execution_1_for_2.clone() { let tp_1_for_2 = DirectedTradingPair::new(trading_pair.asset_1, trading_pair.asset_2); - self.put( - state_key::swap_execution(height, tp_1_for_2), + self.nonverifiable_put( + state_key::swap_execution(height, tp_1_for_2) + .as_bytes() + .to_vec(), swap_execution, ); } diff --git a/crates/proto/src/state/write.rs b/crates/proto/src/state/write.rs index d3367ebf01..57b374cefe 100644 --- a/crates/proto/src/state/write.rs +++ b/crates/proto/src/state/write.rs @@ -22,6 +22,15 @@ pub trait StateWriteProto: StateWrite + Send + Sync { self.put_raw(key, value.encode_to_vec()); } + /// Puts a domain type into the nonverifiable key-value store with the given key + fn nonverifiable_put(&mut self, key: Vec, value: D) + where + D: DomainType, + anyhow::Error: From<>::Error>, + { + self.nonverifiable_put_raw(key, value.encode_to_vec()); + } + /// Records a Protobuf message as a typed ABCI event. fn record_proto(&mut self, proto_event: E) where From bc35acad8eb6d27e8d82fdacacfb67b61d6b3d81 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Tue, 26 Mar 2024 13:27:23 -0400 Subject: [PATCH 041/168] dex: use `put_swap_execution_at_height` --- .../core/component/dex/src/component/dex.rs | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/crates/core/component/dex/src/component/dex.rs b/crates/core/component/dex/src/component/dex.rs index d368ba0a32..bdb9ada0c5 100644 --- a/crates/core/component/dex/src/component/dex.rs +++ b/crates/core/component/dex/src/component/dex.rs @@ -153,7 +153,7 @@ pub trait StateReadExt: StateRead { height: u64, trading_pair: DirectedTradingPair, ) -> Result> { - self.get(&state_key::swap_execution(height, trading_pair)) + self.nonverifiable_get(state_key::swap_execution(height, trading_pair).as_bytes()) .await } @@ -241,19 +241,11 @@ pub trait StateWriteExt: StateWrite + StateReadExt { // Store the swap executions for both directions in the state as well. if let Some(swap_execution) = swap_execution_1_for_2.clone() { let tp_1_for_2 = DirectedTradingPair::new(trading_pair.asset_1, trading_pair.asset_2); - self.nonverifiable_put( - state_key::swap_execution(height, tp_1_for_2) - .as_bytes() - .to_vec(), - swap_execution, - ); + self.put_swap_execution_at_height(height, tp_1_for_2, swap_execution); } if let Some(swap_execution) = swap_execution_2_for_1.clone() { let tp_2_for_1 = DirectedTradingPair::new(trading_pair.asset_2, trading_pair.asset_1); - self.put( - state_key::swap_execution(height, tp_2_for_1), - swap_execution, - ); + self.put_swap_execution_at_height(height, tp_2_for_1, swap_execution); } // ... and also add it to the set in the compact block to be pushed out to clients. @@ -302,6 +294,16 @@ pub trait StateWriteExt: StateWrite + StateReadExt { Ok(()) } + + fn put_swap_execution_at_height( + &mut self, + height: u64, + pair: DirectedTradingPair, + swap_execution: SwapExecution, + ) { + let path = state_key::swap_execution(height, pair); + self.nonverifiable_put(path.as_bytes().to_vec(), swap_execution); + } } impl StateWriteExt for T {} From aa3faf622cae53e165141dbc2b6d86ea8b6e8f5b Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Tue, 26 Mar 2024 13:55:39 -0400 Subject: [PATCH 042/168] upgrades: write a migration script for #4084 --- crates/bin/pd/src/migrate.rs | 88 ++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/crates/bin/pd/src/migrate.rs b/crates/bin/pd/src/migrate.rs index 04feef53f4..d26f9c7e29 100644 --- a/crates/bin/pd/src/migrate.rs +++ b/crates/bin/pd/src/migrate.rs @@ -5,9 +5,10 @@ //! This module declares how local `pd` state should be altered, if at all, //! in order to be compatible with the network post-chain-upgrade. use anyhow::Context; +use futures::StreamExt as _; use std::path::PathBuf; -use cnidarium::{StateDelta, StateWrite, Storage}; +use cnidarium::{StateDelta, StateRead, StateWrite, Storage}; use jmt::RootHash; use penumbra_app::{app::StateReadExt, SUBSTORE_PREFIXES}; use penumbra_sct::component::clock::{EpochManager, EpochRead}; @@ -28,8 +29,8 @@ pub enum Migration { /// A simple migration: adds a key to the consensus state. /// This is useful for testing upgrade mechanisms, including in production. SimpleMigration, - /// Migrates from testnet-64 to testnet-65. - Testnet65, + /// Testnet-70 migration: move swap executions from the jmt to nv-storage. + Testnet70, } impl Migration { @@ -110,7 +111,86 @@ impl Migration { std::fs::write(validator_state_path, fresh_validator_state) .expect("can write validator state"); } - Migration::Testnet65 => { /* currently a no-op. */ } + Migration::Testnet70 => { + // Our goal is to fetch all swap executions from the jmt and store them in nv-storage. + // In particular, we want to make sure that client lookups for (height, trading pair) + // resolve to the same value as before. + + // Setup: + let rocksdb_dir = path_to_export.join("rocksdb"); + let storage = + Storage::load(rocksdb_dir.clone(), SUBSTORE_PREFIXES.to_vec()).await?; + let export_state = storage.latest_snapshot(); + let root_hash = export_state.root_hash().await.expect("can get root hash"); + let _app_hash_pre_migration: RootHash = root_hash.into(); + let pre_upgrade_height = export_state + .get_block_height() + .await + .expect("can get block height"); + let post_upgrade_height = pre_upgrade_height.wrapping_add(1); + + // We initialize a `StateDelta` and start by reaching into the JMT for all entries matching the + // swap execution prefix. Then, we write each entry to the nv-storage. + let mut delta = StateDelta::new(export_state); + + let prefix_key = "dex/swap_execution/"; + let mut swap_execution_stream = delta.prefix_raw(prefix_key); + + while let Some(r) = swap_execution_stream.next().await { + let (key, swap_execution) = r?; + tracing::info!("migrating swap execution: {}", key); + delta.nonverifiable_put_raw(key.into_bytes(), swap_execution); + } + + let post_upgrade_root_hash = storage.commit_in_place(delta).await?; + tracing::info!(?post_upgrade_root_hash, "post-upgrade root hash"); + + // Reload storage so we can make reads against its migrated state: + storage.release().await; + let storage = Storage::load(rocksdb_dir, SUBSTORE_PREFIXES.to_vec()).await?; + let migrated_state = storage.latest_snapshot(); + + // The migration is complete, now we need to generate a genesis file. To do this, we need + // to lookup a validator view from the chain, and specify the post-upgrade app hash and + // initial height. + let chain_id = migrated_state.get_chain_id().await?; + let validators = migrated_state.validator_definitions().await?; + let app_state = penumbra_genesis::Content { + chain_id, + stake_content: StakeContent { + // TODO(erwan): See https://github.com/penumbra-zone/penumbra/issues/3846 + validators: validators.into_iter().map(Into::into).collect(), + ..Default::default() + }, + ..Default::default() + }; + let mut genesis = + TestnetConfig::make_genesis(app_state.clone()).expect("can make genesis"); + genesis.app_hash = post_upgrade_root_hash + .0 + .to_vec() + .try_into() + .expect("infaillible conversion"); + genesis.initial_height = post_upgrade_height as i64; + genesis.genesis_time = genesis_start.unwrap_or_else(|| { + let now = tendermint::time::Time::now(); + tracing::info!(%now, "no genesis time provided, detecting a testing setup"); + now + }); + let checkpoint = post_upgrade_root_hash.0.to_vec(); + let genesis = TestnetConfig::make_checkpoint(genesis, Some(checkpoint)); + + let genesis_json = serde_json::to_string(&genesis).expect("can serialize genesis"); + tracing::info!("genesis: {}", genesis_json); + let genesis_path = path_to_export.join("genesis.json"); + std::fs::write(genesis_path, genesis_json).expect("can write genesis"); + + let validator_state_path = path_to_export.join("priv_validator_state.json"); + let fresh_validator_state = + crate::testnet::generate::TestnetValidator::initial_state(); + std::fs::write(validator_state_path, fresh_validator_state) + .expect("can write validator state"); + } } Ok(()) } From 690fb64a9e756ce4d24876d58cf71026770f8a8a Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Tue, 26 Mar 2024 13:57:16 -0400 Subject: [PATCH 043/168] pd(migrate): use `Testnet70` migration --- crates/bin/pd/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bin/pd/src/main.rs b/crates/bin/pd/src/main.rs index b4bd928966..e8dd635958 100644 --- a/crates/bin/pd/src/main.rs +++ b/crates/bin/pd/src/main.rs @@ -13,7 +13,7 @@ use cnidarium::{StateDelta, Storage}; use metrics_exporter_prometheus::PrometheusBuilder; use pd::{ cli::{Opt, RootCommand, TestnetCommand}, - migrate::Migration::SimpleMigration, + migrate::Migration::Testnet70, testnet::{ config::{get_testnet_dir, parse_tm_address, url_has_necessary_parts}, generate::TestnetConfig, @@ -442,7 +442,7 @@ async fn main() -> anyhow::Result<()> { migrate_archive, } => { tracing::info!("migrating state in {}", target_directory.display()); - SimpleMigration + Testnet70 .migrate(target_directory.clone(), genesis_start) .await .context("failed to upgrade state")?; From ccc8280214e67cb1ba1b7a6baeeb9bdac84ab205 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Wed, 27 Mar 2024 13:48:57 -0400 Subject: [PATCH 044/168] pd(migrate): fix `Testnet70` script --- crates/bin/pd/src/migrate.rs | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/crates/bin/pd/src/migrate.rs b/crates/bin/pd/src/migrate.rs index d26f9c7e29..d68575f5e6 100644 --- a/crates/bin/pd/src/migrate.rs +++ b/crates/bin/pd/src/migrate.rs @@ -12,9 +12,6 @@ use cnidarium::{StateDelta, StateRead, StateWrite, Storage}; use jmt::RootHash; use penumbra_app::{app::StateReadExt, SUBSTORE_PREFIXES}; use penumbra_sct::component::clock::{EpochManager, EpochRead}; -use penumbra_stake::{ - component::validator_handler::ValidatorDataRead, genesis::Content as StakeContent, -}; use crate::testnet::generate::TestnetConfig; @@ -74,14 +71,8 @@ impl Migration { /* ---------- generate genesis ------------ */ let chain_id = migrated_state.get_chain_id().await?; - let validators = migrated_state.validator_definitions().await?; let app_state = penumbra_genesis::Content { chain_id, - stake_content: StakeContent { - // TODO(erwan): See https://github.com/penumbra-zone/penumbra/issues/3846 - validators: validators.into_iter().map(Into::into).collect(), - ..Default::default() - }, ..Default::default() }; let mut genesis = @@ -117,12 +108,13 @@ impl Migration { // resolve to the same value as before. // Setup: + let start_time = std::time::SystemTime::now(); let rocksdb_dir = path_to_export.join("rocksdb"); let storage = Storage::load(rocksdb_dir.clone(), SUBSTORE_PREFIXES.to_vec()).await?; let export_state = storage.latest_snapshot(); let root_hash = export_state.root_hash().await.expect("can get root hash"); - let _app_hash_pre_migration: RootHash = root_hash.into(); + let pre_upgrade_root_hash: RootHash = root_hash.into(); let pre_upgrade_height = export_state .get_block_height() .await @@ -142,9 +134,13 @@ impl Migration { delta.nonverifiable_put_raw(key.into_bytes(), swap_execution); } + delta.put_block_height(0u64); + let post_upgrade_root_hash = storage.commit_in_place(delta).await?; tracing::info!(?post_upgrade_root_hash, "post-upgrade root hash"); + let migration_duration = start_time.elapsed().unwrap(); + // Reload storage so we can make reads against its migrated state: storage.release().await; let storage = Storage::load(rocksdb_dir, SUBSTORE_PREFIXES.to_vec()).await?; @@ -154,14 +150,8 @@ impl Migration { // to lookup a validator view from the chain, and specify the post-upgrade app hash and // initial height. let chain_id = migrated_state.get_chain_id().await?; - let validators = migrated_state.validator_definitions().await?; let app_state = penumbra_genesis::Content { chain_id, - stake_content: StakeContent { - // TODO(erwan): See https://github.com/penumbra-zone/penumbra/issues/3846 - validators: validators.into_iter().map(Into::into).collect(), - ..Default::default() - }, ..Default::default() }; let mut genesis = @@ -190,6 +180,15 @@ impl Migration { crate::testnet::generate::TestnetValidator::initial_state(); std::fs::write(validator_state_path, fresh_validator_state) .expect("can write validator state"); + + tracing::info!( + pre_upgrade_height, + post_upgrade_height, + ?pre_upgrade_root_hash, + ?post_upgrade_root_hash, + duration = migration_duration.as_secs(), + "successful migration!" + ); } } Ok(()) From 42bf675ab6ceb15905fdb026184e8422027f99ea Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Thu, 28 Mar 2024 12:52:01 -0400 Subject: [PATCH 045/168] app: increment halt counter penumbra: update `COMPATIBILTIY.md` app: increment `APP_VERSION` add docstring comment for APP_VERSION --- COMPATIBILITY.md | 3 ++- crates/core/app/src/app/mod.rs | 2 +- crates/core/app/src/lib.rs | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 8e3149a450..493c9938c9 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -1,3 +1,4 @@ | Application version (Name)| Penumbra crate version | CometBFT | Protobuf | | ------------------------- | ---------------------- | -------- | -------- | -| 1 (Testnet 69) | v0.69.1 | v0.37.5 | v1 | +| 1 (Testnet 70) | v0.70.x | v0.37.5 | v1 | +| 2 (Testnet 71) | v0.71.x | v0.37.5 | v1 | diff --git a/crates/core/app/src/app/mod.rs b/crates/core/app/src/app/mod.rs index ac3493a75c..e08991de43 100644 --- a/crates/core/app/src/app/mod.rs +++ b/crates/core/app/src/app/mod.rs @@ -619,7 +619,7 @@ impl App { /// /// Increment this manually after fixing the root cause for a chain halt: updated nodes will then be /// able to proceed past the block height of the halt. -const TOTAL_HALT_COUNT: u64 = 0; +const TOTAL_HALT_COUNT: u64 = 1; #[async_trait] pub trait StateReadExt: StateRead { diff --git a/crates/core/app/src/lib.rs b/crates/core/app/src/lib.rs index f7dfe88b9e..77cabcb848 100644 --- a/crates/core/app/src/lib.rs +++ b/crates/core/app/src/lib.rs @@ -19,7 +19,9 @@ pub use crate::{ use once_cell::sync::Lazy; -pub const APP_VERSION: u64 = 1; +/// Representation of the Penumbra application version. Notably, this is distinct +/// from the crate version(s). This number should only ever be incremented. +pub const APP_VERSION: u64 = 2; pub static SUBSTORE_PREFIXES: Lazy> = Lazy::new(|| { vec![ From b0c8cc44734401e36d3496f17a2ea99a0b1944d1 Mon Sep 17 00:00:00 2001 From: Conor Schaefer Date: Thu, 28 Mar 2024 11:17:32 -0700 Subject: [PATCH 046/168] chore: release version 0.71.0 --- Cargo.lock | 88 +++++++++++++++--------------- Cargo.toml | 2 +- docs/guide/src/penumbra_version.md | 2 +- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6c4ae8eb6..c560afabc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1378,7 +1378,7 @@ dependencies = [ [[package]] name = "cnidarium" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "async-trait", @@ -1413,7 +1413,7 @@ dependencies = [ [[package]] name = "cnidarium-component" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "async-trait", @@ -1892,7 +1892,7 @@ dependencies = [ [[package]] name = "decaf377-fmd" -version = "0.70.3" +version = "0.71.0" dependencies = [ "ark-ff", "ark-serialize", @@ -1907,7 +1907,7 @@ dependencies = [ [[package]] name = "decaf377-frost" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ff", @@ -1922,7 +1922,7 @@ dependencies = [ [[package]] name = "decaf377-ka" -version = "0.70.3" +version = "0.71.0" dependencies = [ "ark-ff", "decaf377 0.5.0", @@ -4397,7 +4397,7 @@ dependencies = [ [[package]] name = "pcli" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ff", @@ -4475,7 +4475,7 @@ dependencies = [ [[package]] name = "pclientd" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "assert_cmd", @@ -4527,7 +4527,7 @@ dependencies = [ [[package]] name = "pd" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ff", @@ -4666,7 +4666,7 @@ dependencies = [ [[package]] name = "penumbra-app" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ff", @@ -4746,7 +4746,7 @@ dependencies = [ [[package]] name = "penumbra-asset" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ff", @@ -4785,7 +4785,7 @@ dependencies = [ [[package]] name = "penumbra-auto-https" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "axum-server", @@ -4797,7 +4797,7 @@ dependencies = [ [[package]] name = "penumbra-bench" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ec", @@ -4841,7 +4841,7 @@ dependencies = [ [[package]] name = "penumbra-community-pool" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ff", @@ -4873,7 +4873,7 @@ dependencies = [ [[package]] name = "penumbra-compact-block" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ff", @@ -4909,7 +4909,7 @@ dependencies = [ [[package]] name = "penumbra-custody" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ff", @@ -4944,7 +4944,7 @@ dependencies = [ [[package]] name = "penumbra-dex" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ff", @@ -5004,7 +5004,7 @@ dependencies = [ [[package]] name = "penumbra-distributions" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "async-trait", @@ -5021,7 +5021,7 @@ dependencies = [ [[package]] name = "penumbra-eddy" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ff", @@ -5039,7 +5039,7 @@ dependencies = [ [[package]] name = "penumbra-fee" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ff", @@ -5064,7 +5064,7 @@ dependencies = [ [[package]] name = "penumbra-funding" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "async-trait", @@ -5086,7 +5086,7 @@ dependencies = [ [[package]] name = "penumbra-genesis" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "penumbra-community-pool", @@ -5105,7 +5105,7 @@ dependencies = [ [[package]] name = "penumbra-governance" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ff", @@ -5162,7 +5162,7 @@ dependencies = [ [[package]] name = "penumbra-ibc" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ff", @@ -5197,7 +5197,7 @@ dependencies = [ [[package]] name = "penumbra-keys" -version = "0.70.3" +version = "0.71.0" dependencies = [ "aes 0.8.4", "anyhow", @@ -5243,7 +5243,7 @@ dependencies = [ [[package]] name = "penumbra-measure" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "bytesize", @@ -5262,7 +5262,7 @@ dependencies = [ [[package]] name = "penumbra-mock-client" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "cnidarium", @@ -5278,7 +5278,7 @@ dependencies = [ [[package]] name = "penumbra-mock-consensus" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "bytes", @@ -5293,7 +5293,7 @@ dependencies = [ [[package]] name = "penumbra-num" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ff", @@ -5330,7 +5330,7 @@ dependencies = [ [[package]] name = "penumbra-proof-params" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ec", @@ -5359,7 +5359,7 @@ dependencies = [ [[package]] name = "penumbra-proof-setup" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ec", @@ -5387,7 +5387,7 @@ dependencies = [ [[package]] name = "penumbra-proto" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "async-trait", @@ -5417,7 +5417,7 @@ dependencies = [ [[package]] name = "penumbra-sct" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ff", @@ -5450,7 +5450,7 @@ dependencies = [ [[package]] name = "penumbra-shielded-pool" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ff", @@ -5500,7 +5500,7 @@ dependencies = [ [[package]] name = "penumbra-stake" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ff", @@ -5553,7 +5553,7 @@ dependencies = [ [[package]] name = "penumbra-tct" -version = "0.70.3" +version = "0.71.0" dependencies = [ "ark-ed-on-bls12-377", "ark-ff", @@ -5584,7 +5584,7 @@ dependencies = [ [[package]] name = "penumbra-tct-property-test" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "futures", @@ -5596,7 +5596,7 @@ dependencies = [ [[package]] name = "penumbra-tct-visualize" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "axum", @@ -5626,7 +5626,7 @@ dependencies = [ [[package]] name = "penumbra-tendermint-proxy" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "chrono", @@ -5657,7 +5657,7 @@ dependencies = [ [[package]] name = "penumbra-tower-trace" -version = "0.70.3" +version = "0.71.0" dependencies = [ "futures", "hex", @@ -5678,7 +5678,7 @@ dependencies = [ [[package]] name = "penumbra-transaction" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-ff", @@ -5730,7 +5730,7 @@ dependencies = [ [[package]] name = "penumbra-txhash" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "blake2b_simd 1.0.2", @@ -5742,7 +5742,7 @@ dependencies = [ [[package]] name = "penumbra-view" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-std", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "penumbra-wallet" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-std", @@ -7609,7 +7609,7 @@ checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" [[package]] name = "summonerd" -version = "0.70.3" +version = "0.71.0" dependencies = [ "anyhow", "ark-groth16", diff --git a/Cargo.toml b/Cargo.toml index bce111bc81..9babc82ef4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,7 +98,7 @@ push = false [workspace.package] authors = ["Penumbra Labs "] edition = "2021" -version = "0.70.3" +version = "0.71.0" repository = "https://github.com/penumbra-zone/penumbra" homepage = "https://penumbra.zone" license = "MIT OR Apache-2.0" diff --git a/docs/guide/src/penumbra_version.md b/docs/guide/src/penumbra_version.md index 7c8613683e..43d60e60b0 100644 --- a/docs/guide/src/penumbra_version.md +++ b/docs/guide/src/penumbra_version.md @@ -1 +1 @@ -v0.70.3 +v0.71.0 From a23ed0ce1427ce85d483406987034cabdfb2e24a Mon Sep 17 00:00:00 2001 From: Conor Schaefer Date: Thu, 28 Mar 2024 11:10:18 -0700 Subject: [PATCH 047/168] docs: provide genesis-start for testnet 71 Refs #4087. Also lists an archive URL for `pd testnet join`. --- docs/guide/src/pd/chain-upgrade.md | 10 +++++++--- docs/guide/src/pd/join-testnet.md | 6 +++--- docs/guide/src/upgrade_genesis_time_70_71.md | 1 + 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 docs/guide/src/upgrade_genesis_time_70_71.md diff --git a/docs/guide/src/pd/chain-upgrade.md b/docs/guide/src/pd/chain-upgrade.md index da6233343d..5e38c46d53 100644 --- a/docs/guide/src/pd/chain-upgrade.md +++ b/docs/guide/src/pd/chain-upgrade.md @@ -23,7 +23,6 @@ After the node is restarted on the new version, it should be able to talk to the Once enough validators with sufficient stake weight have upgraded, the network will resume generating blocks. - ## Genesis time In order for the chain to start again after the upgrade, all nodes must be using the same genesis information, @@ -39,6 +38,11 @@ until the `--genesis-start` time is reached, at which point it will resume proce the community of validators can coordinate resumption of chain activity, even when operators perform migrate their ndoes at slightly different times. +### Testnet 70 -> 71 + +For the first chain upgrade performed on a Penumbra testnet, use this value for genesis time: `{{ #include ../upgrade_genesis_time_70_71.md }}`. +See an example below for how to supply this value when performing the migration. + ## Performing a chain upgrade The following steps assume that `pd` is using the default home directory of `~/.penumbra/testnet_data/node0/pd`. @@ -58,8 +62,8 @@ An example log message emitted by `pd migrate` without providing `--genesis-star The value after `now=` is what should be copied. In practice, for testnets, Penumbra Labs will advise on a genesis time and provide that value in the documentation. Or should we just pick a genesis start ahead of time, and use that for all? --> -5. Apply the migration: `pd migrate --genesis-start "GENESIS_TIME" --target-directory ~/.penumbra/testnet_net/node0/pd-exported-state/ --migrate-archive ~/.penumbra/testnet_data/node0/pd-migrated-state-{{ #include ../penumbra_version.md }}.tar.gz`. - Replace `GENESIS_TIME` with the exact string: `XXXXX`. +5. Apply the migration: `pd migrate --genesis-start "{{ #include ../upgrade_genesis_time_70_71.md }}" --target-directory ~/.penumbra/testnet_net/node0/pd-exported-state/ --migrate-archive ~/.penumbra/testnet_data/node0/pd-migrated-state-{{ #include ../penumbra_version.md }}.tar.gz`. + You must use that precise genesis time, otherwise your node will not be able to reach consensus with the rest of the network. 6. Move the migrated state into place: `mkdir ~/.penumbra/testnet_data/node0/pd && mv ~/.penumbra/testnet_data/node0/pd-exported-state/rocksdb ~/.penumbra/testnet_data/node0/pd/` 7. Move the upgrade cometbft state into place: `cp ~/.penumbra/testnet_data/node0/pd-exported-state/genesis.json ~/.penumbra/testnet_data/node0/cometbft/config/genesis.json && cp ~/.penumbra/testnet_data/pd-exported-state/priv_validator_state.json ~/.penumbra/testnet_data/node0/cometbft/data/priv_validator_state.json` diff --git a/docs/guide/src/pd/join-testnet.md b/docs/guide/src/pd/join-testnet.md index 1c42cad5e8..6a834164d1 100644 --- a/docs/guide/src/pd/join-testnet.md +++ b/docs/guide/src/pd/join-testnet.md @@ -37,10 +37,11 @@ The following section describes how to join a testnet chain *which has never upg Once a chain upgrade occurs, a new-joining node must have access to an archive of historical, migrated state. When we upgrade the chain, we should update these docs to switch to the archive-url version: +--> ```shell pd testnet join --external-address IP_ADDRESS:26656 --moniker MY_NODE_NAME \ - --archive-url "https://snapshots.penumbra.zone/testnet/pd-archived-stated-xxxxx.tar.gz + --archive-url "https://snapshots.penumbra.zone/testnet/pd-migrated-state-70-71.tar.gz" ``` where `IP_ADDRESS` (like `1.2.3.4`) is the public IP address of the node you're running, @@ -48,8 +49,8 @@ and `MY_NODE_NAME` is a moniker identifying your node. Other peers will try to c to your node over port `26656/TCP`. Finally, the `--archive-url` flag will fetch a tarball of historical blocks, so that your newly joining node can understand transactions that occurred prior to the most recent chain upgrade. ---> + diff --git a/docs/guide/src/upgrade_genesis_time_70_71.md b/docs/guide/src/upgrade_genesis_time_70_71.md new file mode 100644 index 0000000000..c28cae3d88 --- /dev/null +++ b/docs/guide/src/upgrade_genesis_time_70_71.md @@ -0,0 +1 @@ +2024-03-28T19:18:38.745760868Z From 8e081b8597ee1a2c7caca51a44f7b8d68a0c49cb Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Thu, 28 Mar 2024 16:08:32 -0400 Subject: [PATCH 048/168] docs: fix typo (#4131) ## Describe your changes Fix a typo in guide. --- docs/guide/src/pd/chain-upgrade.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/src/pd/chain-upgrade.md b/docs/guide/src/pd/chain-upgrade.md index 5e38c46d53..71fbe07436 100644 --- a/docs/guide/src/pd/chain-upgrade.md +++ b/docs/guide/src/pd/chain-upgrade.md @@ -62,7 +62,7 @@ An example log message emitted by `pd migrate` without providing `--genesis-star The value after `now=` is what should be copied. In practice, for testnets, Penumbra Labs will advise on a genesis time and provide that value in the documentation. Or should we just pick a genesis start ahead of time, and use that for all? --> -5. Apply the migration: `pd migrate --genesis-start "{{ #include ../upgrade_genesis_time_70_71.md }}" --target-directory ~/.penumbra/testnet_net/node0/pd-exported-state/ --migrate-archive ~/.penumbra/testnet_data/node0/pd-migrated-state-{{ #include ../penumbra_version.md }}.tar.gz`. +5. Apply the migration: `pd migrate --genesis-start "{{ #include ../upgrade_genesis_time_70_71.md }}" --target-directory ~/.penumbra/testnet_data/node0/pd-exported-state/ --migrate-archive ~/.penumbra/testnet_data/node0/pd-migrated-state-{{ #include ../penumbra_version.md }}.tar.gz`. You must use that precise genesis time, otherwise your node will not be able to reach consensus with the rest of the network. 6. Move the migrated state into place: `mkdir ~/.penumbra/testnet_data/node0/pd && mv ~/.penumbra/testnet_data/node0/pd-exported-state/rocksdb ~/.penumbra/testnet_data/node0/pd/` 7. Move the upgrade cometbft state into place: `cp ~/.penumbra/testnet_data/node0/pd-exported-state/genesis.json ~/.penumbra/testnet_data/node0/cometbft/config/genesis.json From d3014e44c8110cdf83750698226d6fbb93b53e8d Mon Sep 17 00:00:00 2001 From: Conor Schaefer Date: Thu, 28 Mar 2024 14:18:56 -0700 Subject: [PATCH 049/168] docs: fixes another typo in upgrade docs Refs #4087, #4131. --- docs/guide/src/pd/chain-upgrade.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/src/pd/chain-upgrade.md b/docs/guide/src/pd/chain-upgrade.md index 71fbe07436..94d6c21eff 100644 --- a/docs/guide/src/pd/chain-upgrade.md +++ b/docs/guide/src/pd/chain-upgrade.md @@ -66,7 +66,7 @@ and provide that value in the documentation. Or should we just pick a genesis st You must use that precise genesis time, otherwise your node will not be able to reach consensus with the rest of the network. 6. Move the migrated state into place: `mkdir ~/.penumbra/testnet_data/node0/pd && mv ~/.penumbra/testnet_data/node0/pd-exported-state/rocksdb ~/.penumbra/testnet_data/node0/pd/` 7. Move the upgrade cometbft state into place: `cp ~/.penumbra/testnet_data/node0/pd-exported-state/genesis.json ~/.penumbra/testnet_data/node0/cometbft/config/genesis.json - && cp ~/.penumbra/testnet_data/pd-exported-state/priv_validator_state.json ~/.penumbra/testnet_data/node0/cometbft/data/priv_validator_state.json` + && cp ~/.penumbra/testnet_data/node0/pd-exported-state/priv_validator_state.json ~/.penumbra/testnet_data/node0/cometbft/data/priv_validator_state.json` 8. Then we clean up the old CometBFT state: `find ~/.penumbra/testnet_data/node0/cometbft/data/ -mindepth 1 -maxdepth 1 -type d -exec rm -r {} +` Finally, restart the node, e.g. `sudo systemctl restart penumbra cometbft`. Check the logs, and you should see the chain progressing From cba996faa2ec229dd1e4fdca26ff09d7f4711d2e Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Thu, 28 Mar 2024 17:54:17 -0400 Subject: [PATCH 050/168] spec: flesh out more the encrypted parts of a tx (#4105) Signed-off-by: redshiftzero Co-authored-by: katelyn martin --- .../core/component/dex/src/swap/plaintext.rs | 2 +- docs/protocol/src/SUMMARY.md | 1 + .../src/addresses_keys/transaction_crypto.md | 12 +++--- docs/protocol/src/dex/action/swap.md | 38 +++++++++++++++++++ docs/protocol/src/dex/action/swap_claim.md | 4 +- .../src/shielded_pool/action/output.md | 9 +++++ docs/protocol/src/transactions.md | 10 ++--- docs/protocol/src/transactions/actions.md | 6 +-- docs/protocol/src/transactions/memo.md | 16 ++++++++ 9 files changed, 81 insertions(+), 17 deletions(-) create mode 100644 docs/protocol/src/transactions/memo.md diff --git a/crates/core/component/dex/src/swap/plaintext.rs b/crates/core/component/dex/src/swap/plaintext.rs index c138742c45..8bde2f5db6 100644 --- a/crates/core/component/dex/src/swap/plaintext.rs +++ b/crates/core/component/dex/src/swap/plaintext.rs @@ -35,7 +35,7 @@ pub struct SwapPlaintext { pub delta_2_i: Amount, // Prepaid fee to claim the swap pub claim_fee: Fee, - // Address to receive the Swap NFT and SwapClaim outputs + // Address to receive the SwapClaim outputs pub claim_address: Address, // Swap rseed pub rseed: Rseed, diff --git a/docs/protocol/src/SUMMARY.md b/docs/protocol/src/SUMMARY.md index 1a4736043a..2004a4a624 100644 --- a/docs/protocol/src/SUMMARY.md +++ b/docs/protocol/src/SUMMARY.md @@ -51,6 +51,7 @@ - [Transaction Signing](./transactions/signing.md) - [Action Invariants](./transactions/invariants.md) - [Action Reference](./transactions/actions.md) + - [Transaction Memo](./transactions/memo.md) - [Multi-Asset Shielded Pool](./shielded_pool.md) - [Note Plaintexts](./shielded_pool/note_plaintexts.md) - [Note Commitments](./shielded_pool/note_commitments.md) diff --git a/docs/protocol/src/addresses_keys/transaction_crypto.md b/docs/protocol/src/addresses_keys/transaction_crypto.md index d40bff57c9..5d37688e38 100644 --- a/docs/protocol/src/addresses_keys/transaction_crypto.md +++ b/docs/protocol/src/addresses_keys/transaction_crypto.md @@ -7,7 +7,7 @@ For the symmetric encryption described in this section, we use ChaCha20-Poly1305 It is security-critical that `(key, nonce)` pairs are not reused. We do this by either: * deriving per-action keys from ephemeral shared secrets using a fixed nonce (used for notes and memo keys), -* deriving per-action keys from the outgoing viewing key using a nonce derived from a commitment. +* deriving per-action keys from the outgoing viewing key using a nonce derived from a commitment (used for swaps). We use each key at maximum once with a given nonce. We describe the nonces and keys below. @@ -24,7 +24,7 @@ We use a different nonce to encrypt each type of item using the following `[u8; We have several keys involved in transaction-level symmetric cryptography: * A *shared secret* derived between sender and recipient, -* A *per-action payload key* used to encrypt a single note, or memo key (one of each type). +* A *per-action payload key* used to encrypt a single note or memo key (one of each type). It is derived from the shared secret. It is used for a single action. * A *per-action swap payload key* used to encrypt a single swap. It is derived from the outgoing viewing key. It is used for a single action. * A *random payload key* used to encrypt a memo. It is generated randomly and is shared between all @@ -49,7 +49,7 @@ key exchange between: This allows both sender and recipient to generate the shared secret based on the keys they possess. -### Per-action Payload Key: Notes and Memo Keys +### [Per-action Payload Key: Notes and Memo Keys](#per-action-payload-note-memo-key) The symmetric per-action payload key is a 32-byte key derived from the `shared_secret`, the $epk$ and personalization string "Penumbra_Payload": @@ -61,7 +61,7 @@ action_payload_key = BLAKE2b-512("Penumbra_Payload", shared_secret, epk) This symmetric key is then used with the nonces specified above to encrypt a memo key or note. It should not be used to encrypt two items of the same type. -### Per-action Payload Key: Swaps +### [Per-action Payload Key: Swaps](#per-action-swap-key) The symmetric per-action payload key is a 32-byte key derived from the outgoing viewing key `ovk`, the swap commitment `cm`, and personalization string "Penumbra_Payswap": @@ -74,7 +74,7 @@ This symmetric key is used with the nonces specified above to encrypt a swap onl We use the outgoing viewing key for swaps since third parties cannot create swaps for a given recipient, i.e. the user only sends swaps to themselves. -### Random Payload Key: Memos +### [Random Payload Key: Memo](#random-memo-key) The random payload key is a 32-byte key generated randomly. This symmetric key is used with the nonces specified above to encrypt memos only. @@ -96,7 +96,7 @@ outgoing cipher key is to enable the sender to view their outgoing transactions using only their outgoing viewing key. The outgoing cipher key is used to encrypt data that they will need to later reconstruct any outgoing transaction details. -### OVK Wrapped Key +### [OVK Wrapped Key](#ovk-wrapped-key) To decrypt outgoing notes or memo keys, the sender needs to store the shared secret encrypted using the outgoing cipher key described above. This encrypted data, diff --git a/docs/protocol/src/dex/action/swap.md b/docs/protocol/src/dex/action/swap.md index 1d3c59a26e..cf3143cc04 100644 --- a/docs/protocol/src/dex/action/swap.md +++ b/docs/protocol/src/dex/action/swap.md @@ -2,6 +2,44 @@ Each swap contains a SwapBody and a zk-SNARK swap proof. +## [Swap Body](#swap-body) + +The body of an `Swap` has five parts: + +1. A `SwapPayload`, which consists of the swap commitment and the `SwapCiphertext`, +2. A fee balance commitment, which commits to the value balance of the pre-paid fee; +3. The `TradingPair` which is the canonical representation of the two asset IDs involved in the trade, +4. `delta_1_i`, the amount of the _first_ asset ID being traded, +5. `delta_2_i`, the amount of the _second_ asset ID being traded. + +The `SwapCiphertext` is 272 bytes in length, and is encrypted symmetrically using the +payload key derived from the `OutgoingViewingKey` and the swap commitment as +described [here](../../addresses_keys/transaction_crypto.md#per-action-swap-key). + +The corresponding plaintext, `SwapPlaintext` consists of: + +* the `TradingPair` which as above is the canonical representation of the two asset IDs involved in the trade, +* `delta_1_i`, the amount of the _first_ asset ID being traded, +* `delta_2_i`, the amount of the _second_ asset ID being traded, +* the value of the pre-paid claim fee used to claim the outputs of the swap, +* the address used to claim the outputs of the swap, +* the swap `Rseed`, which is used to derive the `Rseed` for the two output notes. + +The output notes for the `Swap` can be computed given the `SwapPlaintext` and the +`BatchSwapOutputData` from that block. The `Rseed` for each output note are computed from +the `SwapPlaintext` using rate-1 Poseidon hashing with domain separators $ds_1$ and $ds_2$ defined as the `Fq` element constructed using: + +`ds_1 = from_le_bytes(BLAKE2b-512(b"penumbra.swapclaim.output1.blinding")) mod q` + +`ds_2 = from_le_bytes(BLAKE2b-512(b"penumbra.swapclaim.output2.blinding")) mod q` + +The rseed for each note is then constructed using the above domain separator and +hashing together the swap rseed $rseed$: + +`rseed_1 = hash_1(ds_1, (rseed))` + +`rseed_2 = hash_1(ds_2, (rseed))` + ## Invariants The invariants that the Swap upholds are described below. diff --git a/docs/protocol/src/dex/action/swap_claim.md b/docs/protocol/src/dex/action/swap_claim.md index f2b707080e..0f5b4aa70e 100644 --- a/docs/protocol/src/dex/action/swap_claim.md +++ b/docs/protocol/src/dex/action/swap_claim.md @@ -46,7 +46,7 @@ The invariants that the SwapClaim upholds are described below. 5. You can only claim swap outputs once via: - 5.1. A swap's transmission key binds to the nullifier key as described in the [Diversified Address Integrity](#diversified-address-integrity) section, and all components of a positioned swap, along with this key, are hashed to derive the nullifier, in circuit as described below in the [Nullifier Integrity](#nullifier-integrity) section. + 5.1. A swap's transmission key binds to the nullifier key as described in the [Nullifier Key Linking](#nullifier-key-linking) section, and all components of a positioned swap, along with this key, are hashed to derive the nullifier, in circuit as described below in the [Nullifier Integrity](#nullifier-integrity) section. 5.2. In the `ActionHandler` for `check_stateful` we check that the nullifier is unspent. @@ -121,7 +121,7 @@ using the witnessed values above and where `ds` is a constant domain separator: as described in [Nullifiers](../../sct/nullifiers.md). -### Nullifier Key Linking +### [Nullifier Key Linking](#nullifier-key-linking) The zk-SNARK certifies that the diversified address $pk_d$ associated with the swap being claimed was derived as: diff --git a/docs/protocol/src/shielded_pool/action/output.md b/docs/protocol/src/shielded_pool/action/output.md index 4baa4c8de2..33785d6a7c 100644 --- a/docs/protocol/src/shielded_pool/action/output.md +++ b/docs/protocol/src/shielded_pool/action/output.md @@ -2,6 +2,15 @@ Each output contains an OutputBody and a zk-SNARK output proof. +## [Output Body](#output-body) + +The body of an `Output` has four parts: + +1. A `NotePayload`, which consists of the note commitment, the `NoteCiphertext`, and an ephemeral public key used to encrypt the note; +2. A balance commitment, which commits to the value balance of the output note; +3. The ovk wrapped key, which enables the _sender_ to later decrypt the `NoteCiphertext` using their `OutgoingViewingKey`; +4. The wrapped memo key, which enables one who can decrypt the `NoteCiphertext` to additionally decrypt the [`MemoCiphertext`](../../transactions/memo.md) on the tranasction. + ## Invariants The invariants that the Output upholds are described below. diff --git a/docs/protocol/src/transactions.md b/docs/protocol/src/transactions.md index 2c38aa735c..2f5cd26c37 100644 --- a/docs/protocol/src/transactions.md +++ b/docs/protocol/src/transactions.md @@ -6,17 +6,17 @@ or providing additional metadata. All actions in the transaction are executed to 1. A list of actions effecting changes to the chain state; 2. A set of `TransactionParameters` describing conditions under which the actions may be executed; -3. A set of `DetectionData` that helps third-party servers detect transactions using [Fuzzy Message Detection](./crypto/fmd.md); -4. A `MemoCiphertext` with an encrypted memo visible only to the sender and receiver(s) of the transaction. +3. An optional set of `DetectionData` that helps third-party servers detect transactions using [Fuzzy Message Detection](./crypto/fmd.md); +4. An optional `MemoCiphertext` with an [encrypted memo](./transactions/memo.md) visible only to the sender and receiver(s) of the transaction. The [Transaction Signing](./transactions/signing.md) section describes transaction authorization. ## Actions and Value Balance -The primary content of a transaction is its list of actions. Each action is executed in sequence, and effects changes to the chain state. +The primary content of a transaction is its list of actions. Each action is executed in sequence, and effects changes to the chain state. Crucially, each action makes a shielded contribution to the transaction's value -balance by means of a _balance commitment_, using the commitment scheme for asset values described in detail in the [Asset Model](./assets.md). +balance by means of a _balance commitment_, using the commitment scheme for asset values described in detail in the [Asset Model](./assets.md). Some actions, like a `Spend`, consume state fragments from the chain and release value into the transaction, while others, like `Output`, consume value from the @@ -33,7 +33,7 @@ prove knowledge of an opening to the commitment, i.e., producing $\widetilde{v}$ such that $$C = [\widetilde{v}] \widetilde{V} = \operatorname{Commit}(0, \widetilde{v}).$$ But this is exactly what it means to create a Schnorr signature for the verification key $C$, because a Schnorr signature is a proof -of knowledge of the signing key in the context of the message. +of knowledge of the signing key in the context of the message. Therefore, we can prove that a value commitment is a commitment to $0$ by treating it as a `decaf377-rdsa` verification key and using the corresponding diff --git a/docs/protocol/src/transactions/actions.md b/docs/protocol/src/transactions/actions.md index 421d76323a..05b9fa4462 100644 --- a/docs/protocol/src/transactions/actions.md +++ b/docs/protocol/src/transactions/actions.md @@ -27,12 +27,12 @@ contributions together. | ------ | ----------- | ------------- | -- | | [`shielded_pool.v1.Spend`](../shielded_pool/action/spend.md) | Spends a note previously included on-chain, releasing its value into the transaction | $+$ (value of spent note) | | | [`shielded_pool.v1.Output`](../shielded_pool/action/output.md) | Produces a new note controlled by a specified address and adds it to the chain state | $-$ (value of new note) | | -| [`dex.v1.Swap`](../dex/action/swap.md) | Submits a swap intent to the chain for batch execution | $-$ (prepaid claim fee) | $-$ (swap inputs) +| [`dex.v1.Swap`](../dex/action/swap.md) | Submits a swap intent to the chain for batch execution | $-$ (prepaid claim fee) | $-$ (swap inputs) | [`dex.v1.SwapClaim`](../dex/action/swap_claim.md) | Claims the outputs of a swap once the clearing price is known, producing new output notes directly | | $+$ (prepaid claim fee) | | `stake.v1.ValidatorDefinition` | Uploads a new validator definition to the chain | | | | `stake.v1.Delegate` | Delegates stake to a validator, exchanging the staking token for that validator's delegation token | | $-$ (staking token) $+$ (delegation token) | `stake.v1.Undelegate` | Undelegates stake from a validator, exchanging delegation tokens for unbonding tokens | | $-$ (delegation token) $+$ (unbonding token) | -| `stake.v1.UndelegateClaim` | Converts unbonding tokens to staking tokens after unbonding completes, at a 1:1 rate unless there are slashing penalties | $-$ (unbonding token) $+$ (staking token) | +| [`stake.v1.UndelegateClaim`](../stake/action/undelegate_claim.md) | Converts unbonding tokens to staking tokens after unbonding completes, at a 1:1 rate unless there are slashing penalties | $-$ (unbonding token) $+$ (staking token) | | `ibc.v1.IbcRelay` | Relays IBC messages from a counterparty chain | | | | `ibc.v1.Ics20Withdrawal` | Initiates an outbound ICS-20 token transfer | | $-$ (transfer amount) | | `dex.v1.PositionOpen` | Opens a liquidity position | | $-$ (initial reserves) $+$ (opened LPNFT) | @@ -42,7 +42,7 @@ contributions together. | `governance.v1.ProposalSubmit` | Submits a governance proposal for voting | TODO | TODO | | `governance.v1.ProposalWithdraw` | Withdraws a governance proposal from voting | TODO | TODO | | `governance.v1.ValidatorVote` | Performs a governance vote as a validator | TODO | TODO | -| `governance.v1.DelegatorVote` | Performs a governance vote as a delegator | TODO | TODO | +| [`governance.v1.DelegatorVote`](../governance/action/delegator_vote.md) | Performs a governance vote as a delegator | TODO | TODO | | `governance.v1.ProposalDepositClaim` | Claims a proposal deposit once voting has finished | TODO | TODO | | `governance.v1.CommunityPoolSpend` | Spends funds from the community pool | | $+$ (spent value) | | `governance.v1.CommunityPoolOutput` | Like `Output`, but transparent | | $-$ (value of new note) diff --git a/docs/protocol/src/transactions/memo.md b/docs/protocol/src/transactions/memo.md new file mode 100644 index 0000000000..79243b3dcd --- /dev/null +++ b/docs/protocol/src/transactions/memo.md @@ -0,0 +1,16 @@ +# Transaction Memo + +The transaction-level memo field is optional, and will be present _if and only if_ the transaction has outputs. A consensus rule will reject transactions with memos that have no outputs, and transactions that have outputs but lack a memo. + +## Memo Plaintext + +The plaintext of the memo contains: + +* a return address (80 bytes for Penumbra addresses) +* a text string that is 432 bytes in length + +## Privacy + +The transaction-level encrypted memo is visible only to the sender and receiver(s) of the transaction. + +Each memo is encrypted using the *Memo Key*, a symmetric ChaCha20-Poly1305 key generated randomly as described [here](../addresses_keys/transaction_crypto.md#random-memo-key). The Memo Key is then encrypted using data from each output following [this section](../addresses_keys/transaction_crypto.md#per-action-payload-key-notes-and-memo-key). This *Wrapped Memo Key* is then added to each individual [Output](../shielded_pool/action/output.md#output-body). From 3155af117839b64ff75cf7ecf7049c5caa3dfc08 Mon Sep 17 00:00:00 2001 From: Jesse Pinho Date: Thu, 28 Mar 2024 15:21:50 -0700 Subject: [PATCH 051/168] proto(stake): add doc to `RateData.validator_exchange_rate` (#4134) --- .../gen/penumbra.core.component.stake.v1.rs | 4 ++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 380322 -> 380567 bytes .../core/component/stake/v1/stake.proto | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/crates/proto/src/gen/penumbra.core.component.stake.v1.rs b/crates/proto/src/gen/penumbra.core.component.stake.v1.rs index 7f5393ea64..ea032a9fc1 100644 --- a/crates/proto/src/gen/penumbra.core.component.stake.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.stake.v1.rs @@ -150,6 +150,10 @@ pub struct RateData { pub validator_reward_rate: ::core::option::Option< super::super::super::num::v1::Amount, >, + /// The validator exchange rate between delegation tokens and staking tokens. + /// The rate is expressed in fixed-point representation with a scaling factor + /// of 10^8. For example, a decimal rate of `1.234` will be represented as + /// `123400000` #[prost(message, optional, tag = "5")] pub validator_exchange_rate: ::core::option::Option< super::super::super::num::v1::Amount, diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 8a26d29b046550f594d789bdc692d546f5431d8b..1c3f308da6ea72846bcbbe27a3080f2455bb9600 100644 GIT binary patch delta 5868 zcmYjV3vgA{6}@NY+ZAnKKq>eezU7k?Jj)g7vc|es+(T^b?>{~6p46!bzX*_?l^xWRr6cQ?+yQ%hK9uh zz2p8)<3#d?@T8QK=BLf2@g;pC`q0WmbX~l$p+4T)yf&J6vTkL(X+*C7rs+PpkEvGUQoE28)7 zsN$5=6EF2eL3HGNU49&oaFpazL$yArIX{Q)lji^2S{fWvItT& z;e3lARa00L&|q3XBLkB_1Pg))(n+u_H)?I5Ef_u`ppl;aL7R;QXZZ3lr$HS%L$i~Qp;TFjJVQFZTuO;F%?1K{X-1(p zz;oHg>4?j6FWbv75azfXmcm#-97(Y5FfY5q%tc!!%lh2~+?Pd%dIOSyJaC;H*>C41 z*U4xKvmIr&@>op09MNx1gL}OklEQGWA2wX)D1$>^PGx3Yx{}Muw7Yaq~8MC=N zHbjQ5^U83EHG;RYY_L~}f`xq9=&V-E?X(}Jx?+$ZwPyjY_!J2~tXbkseE&vyd9{{hGqx$8!V5{ZO6lVK~ zk^S;m%$hsi&IMZ&6pYo<;JyY0cLeXkz4eaZUAVU*c;3+=PUO0vWB@U6*O8f8fMBj0WJ(5vRjxzH z?m@}IzI@7gT5bO)-IIJOG)QZOS!)9scK}FjpiEtFAV_Vl%T#DbuV2Ht;rK`p!h;p6_lJ z&Nd-RD%G3ks6f4XmP(57M+Z%H%NGRQ7rLVXRGHMDxq=5JGLDfcslR2(^iEGSkV#SVe(XMFhG zI|L4-0D`hZASXNd5|D{x%62vW0=-euE-;#n6WU(3i`*d=g3~V0mtCYefJ)8y7nS#x zeuV8^rBY1n0I5VSMxYK5;~5Y=w-_tU^4(p+`8_Hy*Hj?!G2N5D3xmbG54u^reuIc~*q@nl5D?hk8zs zDIgGX4QRR?5Q67KxKs-QLhzgz%=bs8!gyYgDI^f6fR@U!oEKrUN?1WXFAB`GyEl(J z@PZ&~#Z8oXU9f06o5P81WigozD`>AmLt%hHwmc^fn5Px7u>GL`+3aZu= zAS4RVBnl9Tx+pSDjsc;6QA8`OGxRTtiB;J8BnSHLCEP^AnW(p zAq#|n0ZqVw5bU!<77&7cJY=VP=;O|OR$!*FlsqlE>y3G4Z;1lRx zPr7_(mRHnYuh5T@SA?r`!w-?FDC${@uhxhAI81YDr6 z1w9t{@=*9%xcan$FD}=BrIW2yJJ0&LzNY5;?sehZLUJZX7bMm^k!Wdc!H*Cs{~A@~ zT<7Y}K=$)FGdESuHJX{cDad6deu($n6dC5m1PI}qd}HDnJji#y70&m;3?2l}Z$*ed zk)#~NYTpT(u4RCbQNVHw;rpE!t;1zssD3A=PSdIde$w4c&K5EUW&vC`Q^=gd1uXk4 zkxs7!f-ImpvjO4zER~wK07CXz!fzKGY@zRNCFi+7wGf_LDP(q8A**g9x=Tk_NL4Hg zJSM;_yN$+HS_s)~R6WUi1hPfG`vN&T)LUQE!^sz{Vi7C0**Jk92x#I2gkYPE6A*%J zCQd1fxG7$w;Fbb}T>xmdJfOA)mpDLBU!>rck}Tp5dWrBE!Nmgt7tnH9`6UXQVFD|- zFA)Yar>EF=Unb|5L3)be`7(u~x|YSfn06wwm4UDrKy$4Hgl{_yL$Z>1DKKQ)X?#q} zmiX?gfbf0I<_Hj~ui09b`tDA0-Uw7n;klDS zI1pL3lx1JHvOtgp9HvtO2kU$iB|9;THnFO~a~h(}bT0_|T4UAv)EeZ)kR` zGo{F-?WEwk10N`M5)MJW@I|Vg79AxcN4*mqw4#-+K|hes`(t8#KMDD@EZ;cNc|C3gH7v7vYDXIr=T=v3*3N zN~WvmEqqC~-=NAE9+;+!=&Qs&3Y%^L1bH9fy5FeDD9(QM;rE$EP%h)v=uuzZq9w5& z!uLAo6c6FkL%xY05XwCq|1?d3u~)6TO*1NbDL6dK_=8{eQl@DiKz5?R`aFUlw?gl8 R7j6^&(t0o4mVf%o{{hRu=8XUV delta 5623 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 Date: Fri, 29 Mar 2024 07:44:18 -0700 Subject: [PATCH 052/168] penumbra: update minifront/node-status apps (#4132) Pulls in the build outputs of the node-status and minifront apps. --- assets/minifront.zip | 4 ++-- assets/node-status.zip | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/minifront.zip b/assets/minifront.zip index e0eedda6ab..6c77e4cc5f 100644 --- a/assets/minifront.zip +++ b/assets/minifront.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e97fd6115a34a8c0bd837d0d9efd21825403ab96d6c5d5f1308b1948165ea5e3 -size 2860900 +oid sha256:86a561124bfdfab9e421c209af183f6163acaacf338279c4a323c69814ae5943 +size 2902443 diff --git a/assets/node-status.zip b/assets/node-status.zip index 177aa7c9bb..f0780fce86 100644 --- a/assets/node-status.zip +++ b/assets/node-status.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a4c6a83ca8fdcd53abb9d0b40f137e417997a51a0b64ea1a9635c7b8aaaba0f -size 2746854 +oid sha256:7e201c0593227760480a6ffdb5bb5cc7fc6df1f96326a51fe06fab4efffe5ce0 +size 2692222 From 5dbb5481dde718424830d88735bfe8233dcf9fb4 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Wed, 27 Mar 2024 13:20:29 -0400 Subject: [PATCH 053/168] ci: add wasm check for component crates Back in [0] we removed the wasm crate from the monorepo, so that the web team could own it and iterate more quickly. We also purged associated testing, however, so we missed some breakage around the release of v0.70.0. This check attempts to restore some visibility into the potential of breaking APIs relevant to web work. [0] https://github.com/penumbra-zone/penumbra/issues/3819 --- .github/workflows/rust.yml | 10 +++++ deployments/scripts/check-wasm-compat.sh | 54 ++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100755 deployments/scripts/check-wasm-compat.sh diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9d1dff519f..646d7aa973 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -9,12 +9,18 @@ jobs: - uses: actions/checkout@v4 with: lfs: true + - name: Install rust toolchain uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - name: Install nextest uses: taiki-e/install-action@nextest + - name: Load rust cache uses: astriaorg/buildjet-rust-cache@v2.5.1 + - name: Run cargo check, failing on warnings run: cargo check --release --all-targets env: @@ -22,6 +28,9 @@ jobs: # we must duplicate the rustflags from `.cargo/config.toml`. RUSTFLAGS: "-D warnings --cfg tokio_unstable" + - name: Check wasm compatibility + run: ./deployments/scripts/check-wasm-compat.sh + # If a dependency was modified, Cargo.lock may flap if not committed. - name: Check for diffs shell: bash @@ -35,6 +44,7 @@ jobs: else echo "OK: no uncommitted changes detected" fi + - name: Run tests with nextest run: cargo nextest run --release --features migration env: diff --git a/deployments/scripts/check-wasm-compat.sh b/deployments/scripts/check-wasm-compat.sh new file mode 100755 index 0000000000..26d3088874 --- /dev/null +++ b/deployments/scripts/check-wasm-compat.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# CI script for checking that the Penumbra monorepo does not accidentally +# break compatibility with downstream web APIs, via the WASM crate. +# Historically, this breakage has taken the form of inadvertently introducing +# dependencies on std, e.g. via `mio`. +# +# More broadly, we want to ensure that monorepo crates with the "component" +# feature build without that (default) feature enabled. Testing this on the wasm +# target will help ensure compat. +set -euo pipefail + + +# Consider checking the web repo's wasm Cargo.toml periodically: +# +# ❯ rg ^penumbra packages/wasm/crate/Cargo.toml --no-line-number | cut -f1 -d' ' | sort +# +# to make sure at least all of those crates are tracked here. + +packages=( + penumbra-asset + penumbra-community-pool + penumbra-compact-block + penumbra-dex + penumbra-distributions + penumbra-fee + penumbra-funding + penumbra-governance + penumbra-ibc + penumbra-keys + penumbra-sct + penumbra-shielded-pool + penumbra-stake + penumbra-tct + penumbra-transaction + penumbra-txhash + # N.B. we can't include those ones because they rely on `getrandom`, + # but there's a `js` feature... + # decaf377-fmd + # decaf377-frost + # decaf377-ka + # penumbra-num + # penumbra-proof-params + # penumbra-proto +) + +# We intentionally loop over the packages one by one to make error-reporting clearer. +# Ostensibly this would be slow, but in CI with a warm cache it's quick. +for p in "${packages[@]}" ; do + echo "Checking package for wasm compat: $p ..." + if ! cargo check --release --target wasm32-unknown-unknown --no-default-features --package "$p" ; then + >&2 echo "ERROR: package appears not to be wasm-compatible: '$p'" + exit 1 + fi +done From 490c5b6d04ae437a32710adb6503b68f290fc86c Mon Sep 17 00:00:00 2001 From: Jesse Pinho Date: Fri, 29 Mar 2024 11:06:26 -0700 Subject: [PATCH 054/168] fix(pd): minifront asset directory subpath Follow-up to #4132, in which we updated the minifront assets but mistakenly with the wrong subdir nesting. This broke serving minifront from pd's gRPC endpoint, but our tests didn't catch that, so we only noticed when it hit preview. Added a test to the pd integration suite as a sanity check. Made sure to confirm that the test fails on current main, and passes with the asset fix included in this commit. Squashed and committed by @conorsch. --- assets/minifront.zip | 4 ++-- assets/node-status.zip | 4 ++-- crates/bin/pd/tests/network_integration.rs | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/assets/minifront.zip b/assets/minifront.zip index 6c77e4cc5f..c4ad381154 100644 --- a/assets/minifront.zip +++ b/assets/minifront.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86a561124bfdfab9e421c209af183f6163acaacf338279c4a323c69814ae5943 -size 2902443 +oid sha256:4f1cc4f47b93cf1aee49f540b291ad0af3c069455197c27dd7f41c176605a1c9 +size 2901211 diff --git a/assets/node-status.zip b/assets/node-status.zip index f0780fce86..5113e24913 100644 --- a/assets/node-status.zip +++ b/assets/node-status.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e201c0593227760480a6ffdb5bb5cc7fc6df1f96326a51fe06fab4efffe5ce0 -size 2692222 +oid sha256:e831bf9ae8896aeac6a177979584248b9295d819255c14054e4f007fd78931a1 +size 2691710 diff --git a/crates/bin/pd/tests/network_integration.rs b/crates/bin/pd/tests/network_integration.rs index 1739a7ad3b..17585ee39d 100644 --- a/crates/bin/pd/tests/network_integration.rs +++ b/crates/bin/pd/tests/network_integration.rs @@ -4,6 +4,8 @@ //! headers in all contexts. Does NOT evaluate application logic; see the //! integration tests for pcli/pclientd for that. +use http::StatusCode; + #[ignore] #[tokio::test] /// Confirm that permissive CORS headers are returned in HTTP responses @@ -21,3 +23,19 @@ async fn check_cors_headers() -> anyhow::Result<()> { ); Ok(()) } + +#[ignore] +#[tokio::test] +/// Confirm that the a naive GET on the gRPC route returns a 200, +/// as a sanity check that we haven't badly broken the minifront static asset bundling. +/// This check does *not* confirm that page works correctly, but it does confirm +/// it's at least loading, which guards against path regressions in the asset building. +/// See GH4139 for context. +async fn check_minifront_http_ok() -> anyhow::Result<()> { + let client = reqwest::Client::new(); + let pd_url = + std::env::var("PENUMBRA_NODE_PD_URL").unwrap_or("http://localhost:8080".to_string()); + let r = client.get(pd_url).send().await?; + assert_eq!(r.status(), StatusCode::OK); + Ok(()) +} From 74a0b46f63cf42806eda0f198bcf5b1aa93fe5e2 Mon Sep 17 00:00:00 2001 From: Jesse Pinho Date: Sat, 30 Mar 2024 16:43:46 -0700 Subject: [PATCH 055/168] Create ViewService RPC method for requesting unbonding tokens (#4138) --- crates/proto/src/gen/penumbra.view.v1.rs | 195 ++++++++++++ .../proto/src/gen/penumbra.view.v1.serde.rs | 284 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 380567 -> 382593 bytes crates/view/src/service.rs | 14 + proto/penumbra/penumbra/view/v1/view.proto | 37 +++ 5 files changed, 530 insertions(+) diff --git a/crates/proto/src/gen/penumbra.view.v1.rs b/crates/proto/src/gen/penumbra.view.v1.rs index e84195a094..6d931071f6 100644 --- a/crates/proto/src/gen/penumbra.view.v1.rs +++ b/crates/proto/src/gen/penumbra.view.v1.rs @@ -1454,6 +1454,95 @@ impl ::prost::Name for DelegationsByAddressIndexResponse { ::prost::alloc::format!("penumbra.view.v1.{}", Self::NAME) } } +/// Requests unbonding tokens for a given address index, with optional filtering +/// for whether the tokens are currently claimable. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UnbondingTokensByAddressIndexRequest { + #[prost( + enumeration = "unbonding_tokens_by_address_index_request::Filter", + tag = "1" + )] + pub filter: i32, +} +/// Nested message and enum types in `UnbondingTokensByAddressIndexRequest`. +pub mod unbonding_tokens_by_address_index_request { + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] + #[repr(i32)] + pub enum Filter { + /// Return all unbonding tokens, regardless of whether they're claimable + /// right now. + Unspecified = 0, + /// Return all unbonding tokens that are currently claimable. This includes: + /// + /// * tokens that have passed the `unbonding_delay` (from `StakeParameters`) + /// * tokens for unbonded validators + Claimable = 1, + /// Return all unbonding tokens that are not yet claimable, because they are + /// still in the `unbonding_delay` (from `StakeParameters`) period. + NotYetClaimable = 2, + } + impl Filter { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Filter::Unspecified => "FILTER_UNSPECIFIED", + Filter::Claimable => "FILTER_CLAIMABLE", + Filter::NotYetClaimable => "FILTER_NOT_YET_CLAIMABLE", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "FILTER_UNSPECIFIED" => Some(Self::Unspecified), + "FILTER_CLAIMABLE" => Some(Self::Claimable), + "FILTER_NOT_YET_CLAIMABLE" => Some(Self::NotYetClaimable), + _ => None, + } + } + } +} +impl ::prost::Name for UnbondingTokensByAddressIndexRequest { + const NAME: &'static str = "UnbondingTokensByAddressIndexRequest"; + const PACKAGE: &'static str = "penumbra.view.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.view.v1.{}", Self::NAME) + } +} +/// Returns unbonding tokens for the given address index, optionally filtered by +/// whether the tokens are currently claimable. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UnbondingTokensByAddressIndexResponse { + /// A `ValueView` representing the amount of the given unbonding token. + #[prost(message, optional, tag = "1")] + pub value_view: ::core::option::Option, + /// Whether the unbonding token is currently claimable. This will only be + /// `true` if the `unbonding_delay` (from `StakeParameters`) has passed or the + /// validator has unbonded. + #[prost(bool, tag = "2")] + pub claimable: bool, +} +impl ::prost::Name for UnbondingTokensByAddressIndexResponse { + const NAME: &'static str = "UnbondingTokensByAddressIndexResponse"; + const PACKAGE: &'static str = "penumbra.view.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.view.v1.{}", Self::NAME) + } +} /// Generated client implementations. #[cfg(feature = "rpc")] pub mod view_service_client { @@ -2329,6 +2418,40 @@ pub mod view_service_client { ); self.inner.server_streaming(req, path, codec).await } + /// Get unbonding tokens for the given address index, optionally filtered by + /// whether the tokens are currently claimable. + pub async fn unbonding_tokens_by_address_index( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response< + tonic::codec::Streaming, + >, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/penumbra.view.v1.ViewService/UnbondingTokensByAddressIndex", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "penumbra.view.v1.ViewService", + "UnbondingTokensByAddressIndex", + ), + ); + self.inner.server_streaming(req, path, codec).await + } } } /// Generated server implementations. @@ -2659,6 +2782,24 @@ pub mod view_service_server { tonic::Response, tonic::Status, >; + /// Server streaming response type for the UnbondingTokensByAddressIndex method. + type UnbondingTokensByAddressIndexStream: tonic::codegen::tokio_stream::Stream< + Item = std::result::Result< + super::UnbondingTokensByAddressIndexResponse, + tonic::Status, + >, + > + + Send + + 'static; + /// Get unbonding tokens for the given address index, optionally filtered by + /// whether the tokens are currently claimable. + async fn unbonding_tokens_by_address_index( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; } /// The view RPC is used by a view client, who wants to do some /// transaction-related actions, to request data from a view service, which is @@ -4020,6 +4161,60 @@ pub mod view_service_server { }; Box::pin(fut) } + "/penumbra.view.v1.ViewService/UnbondingTokensByAddressIndex" => { + #[allow(non_camel_case_types)] + struct UnbondingTokensByAddressIndexSvc(pub Arc); + impl< + T: ViewService, + > tonic::server::ServerStreamingService< + super::UnbondingTokensByAddressIndexRequest, + > for UnbondingTokensByAddressIndexSvc { + type Response = super::UnbondingTokensByAddressIndexResponse; + type ResponseStream = T::UnbondingTokensByAddressIndexStream; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::UnbondingTokensByAddressIndexRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::unbonding_tokens_by_address_index( + &inner, + request, + ) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = UnbondingTokensByAddressIndexSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.server_streaming(method, req).await; + Ok(res) + }; + Box::pin(fut) + } _ => { Box::pin(async move { Ok( diff --git a/crates/proto/src/gen/penumbra.view.v1.serde.rs b/crates/proto/src/gen/penumbra.view.v1.serde.rs index e0e980002f..6d1dfd7ca3 100644 --- a/crates/proto/src/gen/penumbra.view.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.view.v1.serde.rs @@ -7286,6 +7286,290 @@ impl<'de> serde::Deserialize<'de> for TransactionPlannerResponse { deserializer.deserialize_struct("penumbra.view.v1.TransactionPlannerResponse", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for UnbondingTokensByAddressIndexRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.filter != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.view.v1.UnbondingTokensByAddressIndexRequest", len)?; + if self.filter != 0 { + let v = unbonding_tokens_by_address_index_request::Filter::try_from(self.filter) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.filter)))?; + struct_ser.serialize_field("filter", &v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for UnbondingTokensByAddressIndexRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "filter", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Filter, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "filter" => Ok(GeneratedField::Filter), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = UnbondingTokensByAddressIndexRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.view.v1.UnbondingTokensByAddressIndexRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut filter__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Filter => { + if filter__.is_some() { + return Err(serde::de::Error::duplicate_field("filter")); + } + filter__ = Some(map_.next_value::()? as i32); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(UnbondingTokensByAddressIndexRequest { + filter: filter__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.view.v1.UnbondingTokensByAddressIndexRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for unbonding_tokens_by_address_index_request::Filter { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let variant = match self { + Self::Unspecified => "FILTER_UNSPECIFIED", + Self::Claimable => "FILTER_CLAIMABLE", + Self::NotYetClaimable => "FILTER_NOT_YET_CLAIMABLE", + }; + serializer.serialize_str(variant) + } +} +impl<'de> serde::Deserialize<'de> for unbonding_tokens_by_address_index_request::Filter { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "FILTER_UNSPECIFIED", + "FILTER_CLAIMABLE", + "FILTER_NOT_YET_CLAIMABLE", + ]; + + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = unbonding_tokens_by_address_index_request::Filter; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + fn visit_i64(self, v: i64) -> std::result::Result + where + E: serde::de::Error, + { + i32::try_from(v) + .ok() + .and_then(|x| x.try_into().ok()) + .ok_or_else(|| { + serde::de::Error::invalid_value(serde::de::Unexpected::Signed(v), &self) + }) + } + + fn visit_u64(self, v: u64) -> std::result::Result + where + E: serde::de::Error, + { + i32::try_from(v) + .ok() + .and_then(|x| x.try_into().ok()) + .ok_or_else(|| { + serde::de::Error::invalid_value(serde::de::Unexpected::Unsigned(v), &self) + }) + } + + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "FILTER_UNSPECIFIED" => Ok(unbonding_tokens_by_address_index_request::Filter::Unspecified), + "FILTER_CLAIMABLE" => Ok(unbonding_tokens_by_address_index_request::Filter::Claimable), + "FILTER_NOT_YET_CLAIMABLE" => Ok(unbonding_tokens_by_address_index_request::Filter::NotYetClaimable), + _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + } + } + } + deserializer.deserialize_any(GeneratedVisitor) + } +} +impl serde::Serialize for UnbondingTokensByAddressIndexResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.value_view.is_some() { + len += 1; + } + if self.claimable { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.view.v1.UnbondingTokensByAddressIndexResponse", len)?; + if let Some(v) = self.value_view.as_ref() { + struct_ser.serialize_field("valueView", v)?; + } + if self.claimable { + struct_ser.serialize_field("claimable", &self.claimable)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for UnbondingTokensByAddressIndexResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "value_view", + "valueView", + "claimable", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + ValueView, + Claimable, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "valueView" | "value_view" => Ok(GeneratedField::ValueView), + "claimable" => Ok(GeneratedField::Claimable), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = UnbondingTokensByAddressIndexResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.view.v1.UnbondingTokensByAddressIndexResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut value_view__ = None; + let mut claimable__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::ValueView => { + if value_view__.is_some() { + return Err(serde::de::Error::duplicate_field("valueView")); + } + value_view__ = map_.next_value()?; + } + GeneratedField::Claimable => { + if claimable__.is_some() { + return Err(serde::de::Error::duplicate_field("claimable")); + } + claimable__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(UnbondingTokensByAddressIndexResponse { + value_view: value_view__, + claimable: claimable__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.view.v1.UnbondingTokensByAddressIndexResponse", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for UnclaimedSwapsRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 1c3f308da6ea72846bcbbe27a3080f2455bb9600..1ac5a8dfa6fa8cb7df69cfd56789573d082473ef 100644 GIT binary patch delta 12409 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 delta 10687 zcmYLPd3cu9m4EN{=F1yGk{3dfFCjoS0?C(M5CpXq1repzZaQ_UZKuC6WY^|MENcoZmg?-t*pj z?)fr)Ws#m+G_IYs^LEiL*Q=XtT(8^sb({LH2GO#iaO0_;str-M(I4()QC6&i;2EVq zuaZ6W$fwjC_WY7_PpQJ{ZV{J398O}vRK}v5NXUT2auQ4QjfO>120V!66n$`ly7Pf4 zj8{jwks={zy;OeW4cQ9ds1cv^rEYTf7 zVwmXXGHDmYvI7=aUBf+>#eFUO1rIamojQ2C<5JKm5R*~`$Y_GGX=IBvlg2Bc5 zz(n;oV;3_XIvv`w#Vo6rhcH>pN|MF_5r<1z@RoBxKyxWeR7HOeHbI-aobgpmB)_jq zpHt;}`txc;!EzQQqi9D>5!%w(NeK_CPQ*waJkZf1ow z9)fu@8}Q?SCOHIow1vqsH%iF?nCqb#?Ox;|Y`3svso5JDhuc~38n>S@zC*d4KD>Ld zbdo&UsSiz7_oj9-o-pPRLT4w-wmS_7ww<)o|87h$*s1THqW&Y*$#|tRp@r#WRka?% zq?6UvNB0^N9?L!Y!W1cNpi}wy(}*Aqh!O%%FTjFE@QbbGn=%~Gf6P*V>x-wlvZUQ%eNmFOu@8|6;p1@ zG*(H)azEpCXMjKj=&2N__Ol$jjTi;hepYC|aX?V*XO-2?Dd-;X=cVK!m;gOfXhw5- zIHy7|9boC6eLM!$0ajs83(2!$d64k|ZZHc}fSxJ~R0mm(4=4*%2U#z_TUlT_$ZBex zQ;Ap}V!X{UCBOvenbHYR9b!55Sdu&esza=_!E1x+5NmFARN1jS%=lIlmj;%p;O9brQs@DNl- z*hAlPR5`Ic%6PpS%mEXiXUYN7QI=yr9m#XRbd(kQz0MJ7!UO6YRkv6kW84Sd4OD=h zsvD?|v7BNzQ8!Q>VEyxWBUbJM$%Dy9tgZp4EFB^Fu{^D7rmK6# zoX#8}`7k-nvi(Pq50lfZ$mdW#GVV0xPJ_OI_WO55_>%n6NBMJy2PnIkT{dVKQMmP zT>-$*{{zF|gNE1KNtg6L&QxPtFJ&CEchaSdL-tO(L=KI1h9J7EFV9qCS}!wM;vBMf z(q)#c$q3-kWj0`-TLQ*${C=+gNn;5Eu38AHG6JJf-H;K$%Nwk@#d(Pf_ZH*hxvdbTpv&bJi+Z{l z%%%Ia^AfjjHc3%!(9UIB@@~EI9Iu%vE2RU?Oj5jGnVes7NqLuX^vL;!hA0EpJ^)qu-`S&2OWg<;ZJ3fWy_{4?) z6jM09Ec=0karh$VGrN|aZu%E_)GNY4L5ELBYd?dcyIIao>3Q>2_yfqW#Sr@Jh5%+W zc%_dK0!%Z5Vzkp_#i8bWcGomi1!#`PRjrvNi)ML6w{@t2-+m>NIuJwe6<*!sRsaI< zzQSAE9B(2H=WxETiQrbDG#=Vo{zhThx^F9hJ7OT10a5DFbS zf4*wDy(1H7!X!*bCQt}q*g=7|Izwc`0xkz9EA_6?te>7UU*)ACsCFSFOg=2&H7ywd zC>HSB2H`s95+B1N9xQP+GJOe)c%s7G)aE-_%z2}^41urH=)-K~^wz{($yNuUU?Z73rbtMm0 zJ677%l{{gqUbCgExGBriN&>=LU|Y)Don6I?%1tbGUsv&d6;T9Qd~$1fkTj2d+R?Q< zQDo-Jj>C04*x=^N#?U&RC^k`K(^jwNt~3V1bbzLqN=slUujj?K_5j4{tf$X_!ieH< zBM&w^TB_hS`o)Qec9RW5Ng&Vy+9&{_yU9lZ2-Zy$1+nJD;Z`1O2Wt^^EvTH^N~`mI zT+TT}yG?Ifs2)vk^J@(ubhi1m1_a$Uzt-L2a3>Gmc1}`7x05IOnAp1!+b+L@KyVV! z?jRs^cX?Jou)rQfRU!{5GIyuQvp4n;XGC9dnDh#f@ zCoEAz^3U)@Ve}6W6~y5uJor=}UZQ@H_X$t*LQ6u*0$Q{4`txP#iB|lL_vm@O9Z369Qs}^SrEg^n@|Q;01m1H|oc!3mFqynhQMJb`uKB(p;eXcZ@MX=oj@bma2#9 zE^^oYDKMd5#G}*10f<>Ha`fAN0;l6xUef25s=v#>#1o}a14KRI@J~GWKmCJcDwX#q zo+vZjs2&u@6+L|!x=~lWI}pO1D?Hn_0sz5%g`*Wfq4%IQze=J1EOj+wLJ?f`K|=>7 zSAEbuX!EX7&_C2?maDMwTE>ppuKCEJ2)k<(`5FeNd&c1n9z3smu28G~PJ zNlxG5`uGZUZ__QmB9MaW7SFP896(Ur;zh-%p2-G-;{~tQyH=~aS^@DH1KVdNpcyW< zT>?PZju(|x(c{Jz?zihLE7gx0+J*Z`gh{)=mGC0vYfpj`^~hCfTEj%a zy-!rjOcaGxX49ZUN}_))T;#Ga4kwG?Wu3KJ-P?1rz`fK4qCyIGs^EQ4-8}gRUZ#Jw zS{0@su+1S1gL10KwqFDwD5r|v-RC8KmDNPU%LPrbNWCjGA7Ou%$6o<1#@Ty}gg63>t zZZRo~h;5GGt%e2&8v7`5f%1@4G7jb0>v`CfW|c zZ5IOww1Ace5V{?r@h&qdAZR?&9;N}BBqz4$x7MqX!?p<5rY!NxO z7y~3F(Py}c5)ceq#NFS(V~Qrlk?@wjYlC_u^;Tw5;(beG*;52MFnNnkk#8FlpzV6c z2KAlP_KXRg2-`)L?I|YBiLhN%RGM!;Nj=k@`tCQ?pw!Nc3B6`^3fK6D4tRG8w1ep~ zPf~C6ZT;GtYFO&+j0siHZ;LE@zd#2jZws8V-!vwOyHj`Ds75q&W=yC$?i5+}s7mq> zCY^Lt(N>hAuo1yN+KM4#N}>2Joe@+mmC|nP6|T7l1a3gvf&+x^UQuGJ2tbhT6(~_g zfwT-gQ2pp;RjSK3sm7li6u6`4YwwNHMFdCPr_~z_hv{&n*?QA#M+CPWd?3sQXlp1y z=pGSe_NoWOY)1sH`Vn9)kL7!M%_eo%zc;>PX+IAxNz)jQ##wSFAe$bJ!4^8yG z;LRo(fG`oDJ*)ts`@X2Ky)Zyb^uB0pGGXIdUfKB9g! z(P_c$0R@DK0PSN35W1&D(@^6EASOC3hTmz{sUi-~ir{}8YXvmV(h)@NR}ky4PlW3! z1A-RNW*s1uKM{THEP$Z>M4)f*U1N*E^ZIYMsPCuG`}C+VU&nc$gn%$PPf18SRvCxC z7s02_lSySjsV&c5fHi;MK7ClfMC5yNk^>r1J5y4;GeEl)>7k}OLr}?h?`G7DVpq&p8y4U@DfSB((%}4858;4(s;Fe>pg=YFI zPg_g0H~ifP1X@6w?|@LgAu8-=2MF350q zV;u<1cA2P(THx$JVx1s)O|q)*cYdzVZd3IS0@^D8gV3EI``EAn!8$>@=W<;fPL#nE z$65!?i4rx)NU+us>kE=6lSA~8?W#tP+pfw}5ZIax!_a;~X4|X>1n&zH*W4&$jKRrz z|8_Nc)Z~l_t=nXoZGVfZGwU{4_OcCu`Z#=126LP%_0W7#qRM&@Z1v>IH2wGvH8wTP zvq1=*X)@d1j(~7rn#Ao`Z%i;aU4OVkHKeBdwWv26H{GuVAWWvyT38cYhck8CPMFM; zE=}r9g*;QbAGHBtGE?G5?fcCvK(qAJo$7&xS<>AX_2zXpOQJBL(gKi_B+8PFbk*0> zeK=bucB#$j+0qrD5W?Tt(zSd5VKZBrmJgSyIGihk4!1NZXwLN+m7=AY=K};njsw~R z1BC88>3+-v1nWGBR~&sh4Jbet$lx_c+W^%CGGTvHY#`c&o)!qSfR+{zx(humAZQnQ z+D6ogGFakR8=<*KqRgSMs*zY1BT>!5H1agj0^1bAD3lk=UN%z!LA%&zY7@F%GFahg zo1nRrGL>kXh<2H$1p+IeWd(%pGS3PK)@7D;1leMAx!*b<*aF&z2%s4)wO2hLY?n)1 z^$%eEn&WV_4BpmH|5lC8TP^WKrP`&LoLQ?+{uZ^%TImjN2tl+~y2}X=L~A83C;FkO znS5BMM|7(C)H)vqgd~;y?fC=#n(ersnKkTxUF<*R2UPi{x<#i9#xgv zmNB6sZkx=u$=7U(xNQ>We~mH0FV;Kssy(WbuPpBsQi z+ue{Ybd2qle3)5rAeaK$Yz2hrURgibkHU1X9D0YF7B22XTDd_5m9fB zlSlL&d)0$2NBp|Cnms!rYwSNU0D|s_9DKXE;#z5Ck4b)8a;Uz0ud39i_Tv9aAh69w z44d&P`|<{a@iB>?B`A&B;_!Dec;ETb2F>3|S6sGH8l8}?r2vEo0c{%#5V|L1rELWO zg7t(%OJNvTZ;P|Tk7RI4vSfq4d6k#w`TJCP{zpVjkNDe&`eXU>xFsz!_p9*!kELrJ zVF0urOV>IA1ntKXk3MpCP#m6-!FlKGAZVVE_+OuA7$^r3A0_`v+BZHhwErlxa-+laqZcrdUeW)3P#vwfg6@y`rN)Ty)m)LN z_5ai9!SI@X>X7+t{THsxZ^cjAMPOC}N!IT^~ zrE8f1VG{vuh69>WSGfQprkhl`?5E|qJq~}Zf@k#IN7TNNzgCF>hVga^q+Pi;2M`zm fZGRRJjO{9E-yDEoY*+nje;hvbFTecPU$*`~&a*#9 diff --git a/crates/view/src/service.rs b/crates/view/src/service.rs index 4e6ec60b49..668a318419 100644 --- a/crates/view/src/service.rs +++ b/crates/view/src/service.rs @@ -380,6 +380,13 @@ impl ViewService for ViewServer { + Send, >, >; + type UnbondingTokensByAddressIndexStream = Pin< + Box< + dyn futures::Stream< + Item = Result, + > + Send, + >, + >; async fn broadcast_transaction( &self, @@ -1667,4 +1674,11 @@ impl ViewService for ViewServer { ) -> Result, tonic::Status> { unimplemented!("delegations_by_address_index") } + + async fn unbonding_tokens_by_address_index( + &self, + _request: tonic::Request, + ) -> Result, tonic::Status> { + unimplemented!("unbonding_tokens_by_address_index currently only implemented on web") + } } diff --git a/proto/penumbra/penumbra/view/v1/view.proto b/proto/penumbra/penumbra/view/v1/view.proto index de21d865b7..85b1ef834c 100644 --- a/proto/penumbra/penumbra/view/v1/view.proto +++ b/proto/penumbra/penumbra/view/v1/view.proto @@ -136,6 +136,10 @@ service ViewService { // that token. Each `ValueView`'s `extended_metadata` field will contain the // `ValidatorInfo` of the delegated validator. rpc DelegationsByAddressIndex(DelegationsByAddressIndexRequest) returns (stream DelegationsByAddressIndexResponse); + + // Get unbonding tokens for the given address index, optionally filtered by + // whether the tokens are currently claimable. + rpc UnbondingTokensByAddressIndex(UnbondingTokensByAddressIndexRequest) returns (stream UnbondingTokensByAddressIndexResponse); } message AuthorizeAndBuildRequest { @@ -644,3 +648,36 @@ message DelegationsByAddressIndexRequest { message DelegationsByAddressIndexResponse { core.asset.v1.ValueView value_view = 1; } + +// Requests unbonding tokens for a given address index, with optional filtering +// for whether the tokens are currently claimable. +message UnbondingTokensByAddressIndexRequest { + enum Filter { + // Return all unbonding tokens, regardless of whether they're claimable + // right now. + FILTER_UNSPECIFIED = 0; + + // Return all unbonding tokens that are currently claimable. This includes: + // - tokens that have passed the `unbonding_delay` (from `StakeParameters`) + // - tokens for unbonded validators + FILTER_CLAIMABLE = 1; + + // Return all unbonding tokens that are not yet claimable, because they are + // still in the `unbonding_delay` (from `StakeParameters`) period. + FILTER_NOT_YET_CLAIMABLE = 2; + } + + Filter filter = 1; +} + +// Returns unbonding tokens for the given address index, optionally filtered by +// whether the tokens are currently claimable. +message UnbondingTokensByAddressIndexResponse { + // A `ValueView` representing the amount of the given unbonding token. + core.asset.v1.ValueView value_view = 1; + + // Whether the unbonding token is currently claimable. This will only be + // `true` if the `unbonding_delay` (from `StakeParameters`) has passed or the + // validator has unbonded. + bool claimable = 2; +} From 6428659b97293e608eaf3a03803e7bf008d999b4 Mon Sep 17 00:00:00 2001 From: Jesse Pinho Date: Sat, 30 Mar 2024 17:48:32 -0700 Subject: [PATCH 056/168] Add missing `address_index` field to `UnbondingTokensByAddressIndexRequest` (#4140) ## Describe your changes While working on PR #4138, I forgot to include the `address_index` field in the request for unbonding tokens by address index. This PR fixes that. Relevant changes are to the proto file [here](https://github.com/penumbra-zone/penumbra/pull/4140/files#diff-03b7341d5bf81ab9c8d8542d220d5ba4ae122bd3837531309742b685d7ec1619). ## Issue ticket number and link #4006 ## Checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > Does not affect consensus --- crates/proto/src/gen/penumbra.view.v1.rs | 5 +++++ .../proto/src/gen/penumbra.view.v1.serde.rs | 18 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 382593 -> 382778 bytes proto/penumbra/penumbra/view/v1/view.proto | 3 +++ 4 files changed, 26 insertions(+) diff --git a/crates/proto/src/gen/penumbra.view.v1.rs b/crates/proto/src/gen/penumbra.view.v1.rs index 6d931071f6..6ee1f98e77 100644 --- a/crates/proto/src/gen/penumbra.view.v1.rs +++ b/crates/proto/src/gen/penumbra.view.v1.rs @@ -1464,6 +1464,11 @@ pub struct UnbondingTokensByAddressIndexRequest { tag = "1" )] pub filter: i32, + /// The address index to fetch unbonding tokens for. + #[prost(message, optional, tag = "2")] + pub address_index: ::core::option::Option< + super::super::core::keys::v1::AddressIndex, + >, } /// Nested message and enum types in `UnbondingTokensByAddressIndexRequest`. pub mod unbonding_tokens_by_address_index_request { diff --git a/crates/proto/src/gen/penumbra.view.v1.serde.rs b/crates/proto/src/gen/penumbra.view.v1.serde.rs index 6d1dfd7ca3..7f4bce737c 100644 --- a/crates/proto/src/gen/penumbra.view.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.view.v1.serde.rs @@ -7297,12 +7297,18 @@ impl serde::Serialize for UnbondingTokensByAddressIndexRequest { if self.filter != 0 { len += 1; } + if self.address_index.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("penumbra.view.v1.UnbondingTokensByAddressIndexRequest", len)?; if self.filter != 0 { let v = unbonding_tokens_by_address_index_request::Filter::try_from(self.filter) .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.filter)))?; struct_ser.serialize_field("filter", &v)?; } + if let Some(v) = self.address_index.as_ref() { + struct_ser.serialize_field("addressIndex", v)?; + } struct_ser.end() } } @@ -7314,11 +7320,14 @@ impl<'de> serde::Deserialize<'de> for UnbondingTokensByAddressIndexRequest { { const FIELDS: &[&str] = &[ "filter", + "address_index", + "addressIndex", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { Filter, + AddressIndex, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -7342,6 +7351,7 @@ impl<'de> serde::Deserialize<'de> for UnbondingTokensByAddressIndexRequest { { match value { "filter" => Ok(GeneratedField::Filter), + "addressIndex" | "address_index" => Ok(GeneratedField::AddressIndex), _ => Ok(GeneratedField::__SkipField__), } } @@ -7362,6 +7372,7 @@ impl<'de> serde::Deserialize<'de> for UnbondingTokensByAddressIndexRequest { V: serde::de::MapAccess<'de>, { let mut filter__ = None; + let mut address_index__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Filter => { @@ -7370,6 +7381,12 @@ impl<'de> serde::Deserialize<'de> for UnbondingTokensByAddressIndexRequest { } filter__ = Some(map_.next_value::()? as i32); } + GeneratedField::AddressIndex => { + if address_index__.is_some() { + return Err(serde::de::Error::duplicate_field("addressIndex")); + } + address_index__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -7377,6 +7394,7 @@ impl<'de> serde::Deserialize<'de> for UnbondingTokensByAddressIndexRequest { } Ok(UnbondingTokensByAddressIndexRequest { filter: filter__.unwrap_or_default(), + address_index: address_index__, }) } } diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 1ac5a8dfa6fa8cb7df69cfd56789573d082473ef..d2be9305bfb238b8d714e9072848c7084214b0c8 100644 GIT binary patch delta 332 zcmZo%E52);ctZ*XXd4(R!A^SFZ5)!+Wt40Rhx%#ej=A|o?;dVsQPrq8;{x>sur fD-#=35hF;Hm0bYF1?pMDDlC%3R#22*l5Y$E-8M`l delta 219 zcmdnBPP}ogctZhW-&JHyDuF2u^fu!?p12Nf2P?Gp=F`4SoDZC`MaRgRgblaY&w#Y%{65i7&0 z>6TYnJNOr|a_DjebFr{kF);|SESY}cDr>0T5>_TjE?zEHkRT&alvPRv#$^U^)it3; Date: Mon, 1 Apr 2024 09:45:43 -0400 Subject: [PATCH 057/168] =?UTF-8?q?tests(app):=20=F0=9F=8F=93=20add=20voti?= =?UTF-8?q?ng=20power=20assertions=20to=20staking=20tests=20(#4133)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit see #3995. this addresses points 10b and 11a in #3995. this adds additional assertions to the `app_can_define_and_delegate_to_a_validator` test, which ensure that the validator's voting power changes as expected. #### checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > test changes only --- ..._can_define_and_delegate_to_a_validator.rs | 70 +++++++++++++++++-- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs index 606bd684d5..7c94a4d794 100644 --- a/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs +++ b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs @@ -282,6 +282,7 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { let tx = client.witness_auth_build(&plan).await?; // Execute the transaction, applying it to the chain state. + let pre_delegate_snapshot = storage.latest_snapshot(); node.block() .add_tx(tx.encode_to_vec()) .execute() @@ -292,7 +293,7 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { // Show that the set of validators still looks correct. We should not see any changes yet. { use penumbra_stake::{component::ConsensusIndexRead, validator::State}; - let snapshot = post_delegate_snapshot; + let snapshot = post_delegate_snapshot.clone(); info!("checking consensus set in block after delegation"); // The original validator should still be active. assert_eq!( @@ -315,6 +316,30 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { ); } + // Confirm that the new validator's voting power has not changed immeditaly. + let new_validator_original_power = { + use penumbra_sct::component::clock::EpochRead; + let pre_delegate_power = pre_delegate_snapshot + .get_validator_power(&new_validator_id) + .await? + .expect("should have voting power before delegating"); + let post_delegate_power = post_delegate_snapshot + .get_validator_power(&new_validator_id) + .await? + .expect("should have voting power after delegating"); + debug_assert_eq!( + pre_delegate_snapshot.get_current_epoch().await?, + post_delegate_snapshot.get_current_epoch().await?, + "avoid puzzling errors by confirming that pre- and post-delegation snapshots do not \ + sit upon an epoch boundary" + ); + assert_eq!( + pre_delegate_power, post_delegate_power, + "a delegated validator" + ); + pre_delegate_power + }; + // Fast forward to the next epoch. node.fast_forward(EPOCH_DURATION) .instrument(error_span!( @@ -328,7 +353,7 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { { use penumbra_stake::{component::ConsensusIndexRead, validator::State}; info!("checking consensus set in epoch after delegation"); - let snapshot = post_delegate_next_epoch_snapshot; + let snapshot = post_delegate_next_epoch_snapshot.clone(); // The original validator should still be active. assert_eq!( snapshot.get_validator_state(&existing_validator_id).await?, @@ -349,6 +374,19 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { ); } + // Show that the new validator's voting power has changed, now that we are in a new epoch + // after the delegation. + let new_validator_epoch_after_delegation_power = post_delegate_next_epoch_snapshot + .get_validator_power(&new_validator_id) + .await? + .expect("should have voting power before delegating") + .tap(|&power| { + assert!( + power > new_validator_original_power, + "new validator should now have more voting power after receiving a delegation" + ) + }); + // Build a transaction that will now undelegate from the validator. let plan = { use { @@ -419,7 +457,7 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { // Show that the consensus set has not changed yet. { use penumbra_stake::{component::ConsensusIndexRead, validator::State}; - let snapshot = post_undelegate_snapshot; + let snapshot = post_undelegate_snapshot.clone(); info!("checking consensus set in block after undelegation"); // The original validator should still be active. assert_eq!( @@ -441,6 +479,18 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { ); } + // Compute the current voting power, confirm that it has not changed yet. + post_undelegate_snapshot + .get_validator_power(&new_validator_id) + .await? + .expect("should have voting power before delegating") + .tap(|&power| { + assert_eq!( + power, new_validator_epoch_after_delegation_power, + "validator power should not change immediately after an undelegation" + ) + }); + // Fast forward to the next epoch. node.fast_forward(EPOCH_DURATION) .instrument(error_span!( @@ -454,7 +504,7 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { { use penumbra_stake::{component::ConsensusIndexRead, validator::State}; info!("checking consensus set in epoch after undelegation"); - let snapshot = post_undelegate_next_epoch_snapshot; + let snapshot = post_undelegate_next_epoch_snapshot.clone(); // The original validator should still be active. assert_eq!( snapshot.get_validator_state(&existing_validator_id).await?, @@ -475,6 +525,18 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { ); } + // Show that now, the validator's voting power has returned to its original state. + post_undelegate_next_epoch_snapshot + .get_validator_power(&new_validator_id) + .await? + .expect("should have voting power before delegating") + .tap(|&power| { + assert_eq!( + power, new_validator_original_power, + "validator power should not change immediately after an undelegation" + ) + }); + // The test passed. Free our temporary storage and drop our tracing subscriber. Ok(()) .tap(|_| drop(node)) From ca16ab138a907f5de021861a2c99a641c904e5b4 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Mon, 1 Apr 2024 09:47:49 -0400 Subject: [PATCH 058/168] =?UTF-8?q?tests(app):=20=E2=9C=A8=20add=20integra?= =?UTF-8?q?tion=20test=20coverage=20for=20undelegation=20(#4110)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit this introduces additional test coverage for the staking component, examining the mock client's notes during delegation, the unbonding period, and after claiming an undelegation. * #4115 * #3995 --- Cargo.lock | 1 + .../app_can_undelegate_from_a_validator.rs | 421 ++++++++++++++++++ .../component/action_handler/undelegate.rs | 39 +- crates/test/mock-client/Cargo.toml | 1 + crates/test/mock-client/src/lib.rs | 9 + 5 files changed, 458 insertions(+), 13 deletions(-) create mode 100644 crates/core/app/tests/app_can_undelegate_from_a_validator.rs diff --git a/Cargo.lock b/Cargo.lock index c560afabc8..ca533b5ace 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5266,6 +5266,7 @@ version = "0.71.0" dependencies = [ "anyhow", "cnidarium", + "penumbra-asset", "penumbra-compact-block", "penumbra-dex", "penumbra-keys", diff --git a/crates/core/app/tests/app_can_undelegate_from_a_validator.rs b/crates/core/app/tests/app_can_undelegate_from_a_validator.rs new file mode 100644 index 0000000000..f4eb0c8aee --- /dev/null +++ b/crates/core/app/tests/app_can_undelegate_from_a_validator.rs @@ -0,0 +1,421 @@ +use penumbra_num::fixpoint::U128x128; + +mod common; + +use { + self::common::BuilderExt, + anyhow::anyhow, + ark_ff::UniformRand, + cnidarium::TempStorage, + penumbra_app::server::consensus::Consensus, + penumbra_genesis::AppState, + penumbra_keys::test_keys, + penumbra_mock_client::MockClient, + penumbra_mock_consensus::TestNode, + penumbra_proto::DomainType, + penumbra_sct::component::clock::EpochRead as _, + penumbra_stake::{ + component::validator_handler::ValidatorDataRead as _, validator::Validator, + UndelegateClaimPlan, + }, + penumbra_transaction::{ + memo::MemoPlaintext, plan::MemoPlan, TransactionParameters, TransactionPlan, + }, + rand_core::OsRng, + tap::Tap, + tracing::{error_span, info, Instrument}, +}; + +/// The length of the [`penumbra_sct`] epoch. +/// +/// This test relies on many epochs turning over, so we will work with a shorter epoch duration. +const EPOCH_DURATION: u64 = 3; + +/// The length of the [`penumbra_stake`] unbonding_delay. +const UNBONDING_DELAY: u64 = 4; + +#[tokio::test] +async fn app_can_undelegate_from_a_validator() -> anyhow::Result<()> { + // Install a test logger, acquire some temporary storage, and start the test node. + let guard = common::set_tracing_subscriber(); + let storage = TempStorage::new().await?; + + // Helper function to get the latest block height. + let get_latest_height = || async { + storage + .latest_snapshot() + .get_block_height() + .await + .expect("should be able to get latest block height") + }; + + // Helper function to get the latest epoch. + let get_latest_epoch = || async { + storage + .latest_snapshot() + .get_current_epoch() + .await + .expect("should be able to get curent epoch") + }; + + // Configure an AppState with slightly shorter epochs than usual. + let app_state = AppState::Content(penumbra_genesis::Content { + sct_content: penumbra_sct::genesis::Content { + sct_params: penumbra_sct::params::SctParameters { + epoch_duration: EPOCH_DURATION, + }, + }, + stake_content: penumbra_stake::genesis::Content { + stake_params: penumbra_stake::params::StakeParameters { + unbonding_delay: UNBONDING_DELAY, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }); + + // Start the test node. + let mut node = { + let consensus = Consensus::new(storage.as_ref().clone()); + TestNode::builder() + .single_validator() + .with_penumbra_auto_app_state(app_state)? + .init_chain(consensus) + .await + }?; + + // Retrieve the validator definition from the latest snapshot. + let Validator { identity_key, .. } = match storage + .latest_snapshot() + .validator_definitions() + .tap(|_| info!("getting validator definitions")) + .await? + .as_slice() + { + [v] => v.clone(), + unexpected => panic!("there should be one validator, got: {unexpected:?}"), + }; // ..and note the asset id for delegation tokens tied to this validator. + let delegate_token_id = penumbra_stake::DelegationToken::new(identity_key).id(); + + // Sync the mock client, using the test wallet's spend key, to the latest snapshot. + let mut client = MockClient::new(test_keys::SPEND_KEY.clone()) + .with_sync_to_storage(&storage) + .await? + .tap(|c| info!(client.notes = %c.notes.len(), "mock client synced to test storage")); + + // Proceed into the third epoch, so that the exchange rate isn't 1. + { + let target = 2; + while get_latest_epoch().await.index < target { + node.block().execute().await?; + } + } + + // Now, create a transaction that delegates to the validator. + // + // Hang onto the staking note nullifier, so we can interrogate whether that note is spent. + let delegate_rate = storage + .latest_snapshot() + .get_validator_rate(&identity_key) + .await? + .ok_or(anyhow!("validator has a rate"))? + .tap(|rate| tracing::info!(?rate, "got validator rate")); + let (plan, staking_note, staking_note_nullifier) = { + use { + penumbra_shielded_pool::{OutputPlan, SpendPlan}, + penumbra_transaction::{ + memo::MemoPlaintext, plan::MemoPlan, TransactionParameters, TransactionPlan, + }, + }; + let note = client + .notes_by_asset(*penumbra_asset::STAKING_TOKEN_ASSET_ID) + .cloned() + .next() + .expect("should get staking note"); + let spend = SpendPlan::new( + &mut rand_core::OsRng, + note.clone(), + client + .position(note.commit()) + .expect("note should be in mock client's tree"), + ); + let staking_note_nullifier = spend.nullifier(&client.fvk); + let delegate = delegate_rate.build_delegate( + storage.latest_snapshot().get_current_epoch().await?, + note.amount(), + ); + let output = OutputPlan::new( + &mut rand_core::OsRng, + delegate.delegation_value(), + *test_keys::ADDRESS_1, + ); + let mut plan = TransactionPlan { + actions: vec![spend.into(), output.into(), delegate.into()], + // Now fill out the remaining parts of the transaction needed for verification: + memo: MemoPlan::new(&mut OsRng, MemoPlaintext::blank_memo(*test_keys::ADDRESS_0)) + .map(Some)?, + detection_data: None, // We'll set this automatically below + transaction_parameters: TransactionParameters { + chain_id: TestNode::<()>::CHAIN_ID.to_string(), + ..Default::default() + }, + }; + plan.populate_detection_data(rand_core::OsRng, 0); + (plan, note, staking_note_nullifier) + }; + let tx = client.witness_auth_build(&plan).await?; + + // Show that the client does not have delegation tokens before delegating. + assert_eq!( + client.notes_by_asset(delegate_token_id).count(), + 0, + "client should not have delegation tokens before delegating" + ); + + // Execute the transaction, applying it to the chain state. + node.block() + .add_tx(tx.encode_to_vec()) + .execute() + .instrument(error_span!("executing block with delegation transaction")) + .await?; + let post_delegate_snapshot = storage.latest_snapshot(); + client + .sync_to_latest(post_delegate_snapshot.clone()) + .await?; + + // Show that the client now has a single note for some delegation tokens. + let delegate_note: penumbra_shielded_pool::Note = { + let mut notes: Vec<_> = client.notes_by_asset(delegate_token_id).cloned().collect(); + assert_eq!(notes.len(), 1, "client should now have delegation tokens"); + notes.pop().unwrap() + }; + + // Show that the staking note has a nullifier that has now been spent. + { + use penumbra_sct::component::tree::VerificationExt; + let snapshot = storage.latest_snapshot(); + let Err(_) = snapshot + .check_nullifier_unspent(staking_note_nullifier) + .await + else { + panic!("staking note was spent in delegation") + }; + } + + // Fast forward to the next epoch. + { + let start = get_latest_epoch().await.index; + while get_latest_epoch().await.index < start { + node.block().execute().await?; + } + } + + // Build a transaction that will now undelegate from the validator. + let undelegate_rate = storage + .latest_snapshot() + .get_validator_rate(&identity_key) + .await? + .ok_or(anyhow::anyhow!("new validator has a rate"))? + .tap(|rate| tracing::info!(?rate, "got new validator rate")); + let (plan, undelegate_token_id) = { + use { + penumbra_shielded_pool::{OutputPlan, SpendPlan}, + penumbra_stake::DelegationToken, + penumbra_transaction::{ + memo::MemoPlaintext, plan::MemoPlan, TransactionParameters, TransactionPlan, + }, + }; + let snapshot = storage.latest_snapshot(); + client.sync_to_latest(snapshot.clone()).await?; + let undelegation_id = DelegationToken::new(identity_key).id(); + let note = client + .notes + .values() + .filter(|n| n.asset_id() == undelegation_id) + .cloned() + .next() + .expect("the test account should have one staking token note"); + let spend = SpendPlan::new( + &mut rand_core::OsRng, + note.clone(), + client + .position(note.commit()) + .expect("note should be in mock client's tree"), + ); + let undelegate = undelegate_rate.build_undelegate( + storage.latest_snapshot().get_current_epoch().await?, + note.amount(), + ); + let undelegate_token_id = undelegate.unbonding_token().id(); + let output = OutputPlan::new( + &mut rand_core::OsRng, + undelegate.unbonded_value(), + *test_keys::ADDRESS_1, + ); + let mut plan = TransactionPlan { + actions: vec![spend.into(), output.into(), undelegate.into()], + // Now fill out the remaining parts of the transaction needed for verification: + memo: MemoPlan::new(&mut OsRng, MemoPlaintext::blank_memo(*test_keys::ADDRESS_0)) + .map(Some)?, + detection_data: None, // We'll set this automatically below + transaction_parameters: TransactionParameters { + chain_id: TestNode::<()>::CHAIN_ID.to_string(), + ..Default::default() + }, + }; + plan.populate_detection_data(rand_core::OsRng, 0); + (plan, undelegate_token_id) + }; + let tx = client.witness_auth_build(&plan).await?; + + // Execute the undelegation transaction, applying it to the chain state. + let pre_undelegated_epoch = get_latest_epoch().await; + node.block() + .add_tx(tx.encode_to_vec()) + .execute() + .instrument(error_span!("executing block with undelegation transaction")) + .await?; + let post_undelegate_snapshot = storage.latest_snapshot(); + + // Compute the height we expect to see this unbonding period finish. + let expected_end_of_unboding_period_height = post_undelegate_snapshot + .compute_unbonding_height( + &identity_key, + post_undelegate_snapshot.get_block_height().await?, + ) + .await? + .expect("snapshot should have a block height"); + + // Show that we immediately receive unbonding tokens after undelegating. + let undelegate_note: penumbra_shielded_pool::Note = { + client.sync_to_latest(post_undelegate_snapshot).await?; + let mut undelegate_notes: Vec<_> = client + .notes_by_asset(undelegate_token_id) + .cloned() + .collect(); + assert_eq!( + undelegate_notes.len(), + 1, + "client should have unbonding tokens immediately after undelegating" + ); + assert_eq!( + client.notes_by_asset(delegate_token_id).count(), + /*0, TODO(kate): we still see delegation tokens after undelegating*/ 1, + "client should not have delegation tokens immediately after undelegating" + ); + undelegate_notes.pop().unwrap() + }; + + // Jump to the end of the unbonding period. + { + let jump_to = expected_end_of_unboding_period_height; + while get_latest_height().await < jump_to { + node.block().execute().await?; + } + } + + // Build a transaction that will now reclaim staking tokens from the validator. + let plan = { + client.sync_to_latest(storage.latest_snapshot()).await?; + let penalty = penumbra_stake::Penalty::from_percent(0); + let note = client + .notes + .values() + .cloned() + .filter(|n| n.asset_id() == undelegate_token_id) + .next() + .expect("should have an unbonding note"); + let claim = UndelegateClaimPlan { + validator_identity: identity_key, + unbonding_start_height: pre_undelegated_epoch.start_height, + penalty, + unbonding_amount: note.amount(), + balance_blinding: decaf377::Fr::rand(&mut OsRng), + proof_blinding_r: decaf377::Fq::rand(&mut OsRng), + proof_blinding_s: decaf377::Fq::rand(&mut OsRng), + }; + let mut plan = TransactionPlan { + actions: vec![claim.into()], + // Now fill out the remaining parts of the transaction needed for verification: + memo: MemoPlan::new(&mut OsRng, MemoPlaintext::blank_memo(*test_keys::ADDRESS_0)) + .map(Some)?, + detection_data: None, // We'll set this automatically below + transaction_parameters: TransactionParameters { + chain_id: TestNode::<()>::CHAIN_ID.to_string(), + ..Default::default() + }, + }; + plan.populate_detection_data(rand_core::OsRng, 0); + plan + }; + let tx = client.witness_auth_build(&plan).await?; + + // Execute the transaction, applying it to the chain state. + node.block() + .add_tx(tx.encode_to_vec()) + .execute() + .instrument(error_span!("executing block with undelegation claim")) + .await?; + let post_claim_snapshot = storage.latest_snapshot(); + + { + client.sync_to_latest(post_claim_snapshot.clone()).await?; + let notes: Vec<_> = client + .notes_by_asset(*penumbra_asset::STAKING_TOKEN_ASSET_ID) + .cloned() + .collect(); + assert_eq!(notes.len(), 1, "client should still have staking notes"); + assert_eq!( + client.notes_by_asset(undelegate_token_id).count(), + 1, + "client should still have undelegation notes" + ); + assert_eq!( + client.notes_by_asset(delegate_token_id).count(), + 1, + "client should still have delegation notes" + ); + }; + + // Lets make some assertions that the note amounts respect the validator rates. + { + use penumbra_stake::BPS_SQUARED_SCALING_FACTOR; + use std::ops::Deref; + + let staking_note_amount: U128x128 = staking_note.amount().into(); + let delegate_note_amount: U128x128 = delegate_note.amount().into(); + + let delegate_exchange_rate: U128x128 = delegate_rate.validator_exchange_rate.into(); + let undelegate_exchange_rate: U128x128 = undelegate_rate.validator_exchange_rate.into(); + let scaled_delegate_rate: U128x128 = + (delegate_exchange_rate / BPS_SQUARED_SCALING_FACTOR.deref())?; + let scaled_undelegate_rate: U128x128 = + (undelegate_exchange_rate / BPS_SQUARED_SCALING_FACTOR.deref())?; + + // Compute the expected amount of delegation tokens we should have received. + let expected: penumbra_num::Amount = staking_note_amount + .checked_div(&scaled_delegate_rate)? + .round_down() + .try_into()?; + assert_eq!( + delegate_note.amount(), + expected, + "the ratio of delegation tokens to staking tokens should reflect the validator's \ + exchange rate at time of delegation", + ); + + // Compute the expected amount of undelegation tokens we should have received. + let expected: penumbra_num::Amount = delegate_note_amount + .checked_mul(&scaled_undelegate_rate)? + .round_down() + .try_into()?; + assert_eq!(undelegate_note.amount(), expected, "let's learn something"); + } + + // The test passed. Free our temporary storage and drop our tracing subscriber. + Ok(()) + .tap(|_| drop(node)) + .tap(|_| drop(storage)) + .tap(|_| drop(guard)) +} diff --git a/crates/core/component/stake/src/component/action_handler/undelegate.rs b/crates/core/component/stake/src/component/action_handler/undelegate.rs index 207aa13036..63b765a30a 100644 --- a/crates/core/component/stake/src/component/action_handler/undelegate.rs +++ b/crates/core/component/stake/src/component/action_handler/undelegate.rs @@ -1,4 +1,4 @@ -use anyhow::{ensure, Result}; +use anyhow::Result; use async_trait::async_trait; use cnidarium::StateWrite; use penumbra_sct::component::clock::EpochRead; @@ -27,12 +27,19 @@ impl ActionHandler for Undelegate { // This let us provide a more helpful error message if an epoch boundary was crossed. // And it ensures that the unbonding delay is enforced correctly. let current_epoch = state.get_current_epoch().await?; - ensure!( - u.from_epoch == current_epoch, - "undelegation was prepared for epoch {} but the current epoch is {}", - u.from_epoch.index, - current_epoch.index - ); + let prepared_for_current_epoch = u.from_epoch == current_epoch; + if !prepared_for_current_epoch { + tracing::error!( + from = ?u.from_epoch, + current = ?current_epoch, + "undelegation was prepared for a different epoch", + ); + anyhow::bail!( + "undelegation was prepared for epoch {} but the current epoch is {}", + u.from_epoch.index, + current_epoch.index + ); + } // For undelegations, we enforce correct computation (with rounding) // of the *unbonded amount based on the delegation amount*, because @@ -56,12 +63,18 @@ impl ActionHandler for Undelegate { })?; let expected_unbonded_amount = rate_data.unbonded_amount(u.delegation_amount); - ensure!( - u.unbonded_amount == expected_unbonded_amount, - "undelegation amount {} does not match expected amount {}", - u.unbonded_amount, - expected_unbonded_amount, - ); + if u.unbonded_amount != expected_unbonded_amount { + tracing::error!( + actual = %u.unbonded_amount, + expected = %expected_unbonded_amount, + "undelegation amount does not match expected amount", + ); + anyhow::bail!( + "undelegation amount {} does not match expected amount {}", + u.unbonded_amount, + expected_unbonded_amount, + ); + } /* ----- execution ------ */ diff --git a/crates/test/mock-client/Cargo.toml b/crates/test/mock-client/Cargo.toml index 6d8baae17c..b05dc6f2b3 100644 --- a/crates/test/mock-client/Cargo.toml +++ b/crates/test/mock-client/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true [dependencies] anyhow = {workspace = true} cnidarium = {workspace = true, default-features = true} +penumbra-asset = {workspace = true} penumbra-compact-block = {workspace = true, default-features = true} penumbra-dex = {workspace = true, default-features = true} penumbra-keys = {workspace = true, default-features = true} diff --git a/crates/test/mock-client/src/lib.rs b/crates/test/mock-client/src/lib.rs index 1f83193834..851eed7b5c 100644 --- a/crates/test/mock-client/src/lib.rs +++ b/crates/test/mock-client/src/lib.rs @@ -200,4 +200,13 @@ impl MockClient { .build_concurrent(&self.fvk, &witness_data, &auth_data) .await } + + pub fn notes_by_asset( + &self, + asset_id: penumbra_asset::asset::Id, + ) -> impl Iterator + '_ { + self.notes + .values() + .filter(move |n| n.asset_id() == asset_id) + } } From 03a8c114451e72adaefe763c80915090da5ba3f2 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Mon, 1 Apr 2024 16:03:41 -0400 Subject: [PATCH 059/168] =?UTF-8?q?refactor(app):=20=F0=9F=93=8E=20fold=20?= =?UTF-8?q?`genesis::{AppState,=20Content}`=20into=20`penumbra-app`=20(#41?= =?UTF-8?q?42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes #4088. see also #3761, 95a9d35a2. this moves the genesis state for the application back into the `penumbra_app` library, so that we maintain parity with our protobuf definitions. --- Cargo.lock | 21 ----------------- Cargo.toml | 2 -- crates/bin/pd/Cargo.toml | 1 - crates/bin/pd/src/migrate.rs | 4 ++-- crates/bin/pd/src/testnet/config.rs | 2 +- crates/bin/pd/src/testnet/generate.rs | 22 +++++++++--------- crates/core/app/Cargo.toml | 1 - crates/core/app/src/app/mod.rs | 2 +- .../src/lib.rs => app/src/genesis.rs} | 0 crates/core/app/src/lib.rs | 1 + crates/core/app/src/server/consensus.rs | 6 ++--- ..._can_define_and_delegate_to_a_validator.rs | 5 ++-- .../app_can_undelegate_from_a_validator.rs | 5 ++-- ...me_for_genesis_validator_missing_blocks.rs | 3 +-- ...me_for_genesis_validator_signing_blocks.rs | 3 +-- ..._uptime_for_validators_only_once_active.rs | 5 ++-- crates/core/app/tests/common/mod.rs | 2 +- .../app/tests/common/test_node_builder_ext.rs | 2 +- crates/core/genesis/Cargo.toml | 23 ------------------- deployments/scripts/rust-docs | 1 - 20 files changed, 29 insertions(+), 82 deletions(-) rename crates/core/{genesis/src/lib.rs => app/src/genesis.rs} (100%) delete mode 100644 crates/core/genesis/Cargo.toml diff --git a/Cargo.lock b/Cargo.lock index ca533b5ace..b65a4d8260 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4571,7 +4571,6 @@ dependencies = [ "penumbra-custody", "penumbra-dex", "penumbra-fee", - "penumbra-genesis", "penumbra-governance", "penumbra-ibc", "penumbra-keys", @@ -4698,7 +4697,6 @@ dependencies = [ "penumbra-distributions", "penumbra-fee", "penumbra-funding", - "penumbra-genesis", "penumbra-governance", "penumbra-ibc", "penumbra-keys", @@ -5084,25 +5082,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "penumbra-genesis" -version = "0.71.0" -dependencies = [ - "anyhow", - "penumbra-community-pool", - "penumbra-dex", - "penumbra-distributions", - "penumbra-fee", - "penumbra-funding", - "penumbra-governance", - "penumbra-ibc", - "penumbra-proto", - "penumbra-sct", - "penumbra-shielded-pool", - "penumbra-stake", - "serde", -] - [[package]] name = "penumbra-governance" version = "0.71.0" diff --git a/Cargo.toml b/Cargo.toml index 9babc82ef4..971a549bb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,6 @@ members = [ "crates/core/component/sct", "crates/core/component/shielded-pool", "crates/core/component/stake", - "crates/core/genesis", "crates/core/keys", "crates/core/num", "crates/core/transaction", @@ -169,7 +168,6 @@ penumbra-distributions = { default-features = false, path = "crates/co penumbra-fee = { default-features = false, path = "crates/core/component/fee" } penumbra-funding = { default-features = false, path = "crates/core/component/funding" } penumbra-governance = { default-features = false, path = "crates/core/component/governance" } -penumbra-genesis = { path = "crates/core/genesis" } penumbra-ibc = { default-features = false, path = "crates/core/component/ibc" } penumbra-keys = { default-features = false, path = "crates/core/keys" } penumbra-mock-client = { path = "crates/test/mock-client" } diff --git a/crates/bin/pd/Cargo.toml b/crates/bin/pd/Cargo.toml index 9951853bb1..b729d89fd5 100644 --- a/crates/bin/pd/Cargo.toml +++ b/crates/bin/pd/Cargo.toml @@ -73,7 +73,6 @@ penumbra-compact-block = { workspace = true, default-features = true } penumbra-custody = { workspace = true } penumbra-dex = { workspace = true, features = ["parallel"], default-features = true } penumbra-fee = { workspace = true, default-features = true } -penumbra-genesis = { workspace = true } penumbra-governance = { workspace = true, features = ["parallel"], default-features = true } penumbra-ibc = { workspace = true, features = ["rpc"], default-features = true } penumbra-keys = { workspace = true, default-features = true } diff --git a/crates/bin/pd/src/migrate.rs b/crates/bin/pd/src/migrate.rs index d68575f5e6..922d0bc001 100644 --- a/crates/bin/pd/src/migrate.rs +++ b/crates/bin/pd/src/migrate.rs @@ -71,7 +71,7 @@ impl Migration { /* ---------- generate genesis ------------ */ let chain_id = migrated_state.get_chain_id().await?; - let app_state = penumbra_genesis::Content { + let app_state = penumbra_app::genesis::Content { chain_id, ..Default::default() }; @@ -150,7 +150,7 @@ impl Migration { // to lookup a validator view from the chain, and specify the post-upgrade app hash and // initial height. let chain_id = migrated_state.get_chain_id().await?; - let app_state = penumbra_genesis::Content { + let app_state = penumbra_app::genesis::Content { chain_id, ..Default::default() }; diff --git a/crates/bin/pd/src/testnet/config.rs b/crates/bin/pd/src/testnet/config.rs index ead16efc52..5f6d534e33 100644 --- a/crates/bin/pd/src/testnet/config.rs +++ b/crates/bin/pd/src/testnet/config.rs @@ -1,8 +1,8 @@ use anyhow::Context; use decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey}; use directories::UserDirs; +use penumbra_app::genesis::AppState; use penumbra_custody::soft_kms::Config as SoftKmsConfig; -use penumbra_genesis::AppState; use penumbra_keys::keys::{SpendKey, SpendKeyBytes}; use rand::Rng; use rand_core::OsRng; diff --git a/crates/bin/pd/src/testnet/generate.rs b/crates/bin/pd/src/testnet/generate.rs index 76fcd6d85b..f063c02afd 100644 --- a/crates/bin/pd/src/testnet/generate.rs +++ b/crates/bin/pd/src/testnet/generate.rs @@ -35,7 +35,7 @@ pub struct TestnetConfig { /// The name of the network pub name: String, /// The Tendermint genesis for initial chain state. - pub genesis: Genesis, + pub genesis: Genesis, /// Path to local directory where config files will be written to pub testnet_dir: PathBuf, /// Set of validators at genesis. Uses the convenient wrapper type @@ -186,7 +186,7 @@ impl TestnetConfig { epoch_duration: Option, unbonding_delay: Option, proposal_voting_blocks: Option, - ) -> anyhow::Result { + ) -> anyhow::Result { let default_gov_params = penumbra_governance::params::GovernanceParameters::default(); let gov_params = penumbra_governance::params::GovernanceParameters { @@ -198,7 +198,7 @@ impl TestnetConfig { // Look up default app params, so we can fill in defaults. let default_app_params = AppParameters::default(); - let app_state = penumbra_genesis::Content { + let app_state = penumbra_app::genesis::Content { chain_id: chain_id.to_string(), stake_content: StakeContent { validators: validators.into_iter().map(Into::into).collect(), @@ -230,8 +230,8 @@ impl TestnetConfig { /// Build Tendermint genesis data, based on Penumbra initial application state. pub(crate) fn make_genesis( - app_state: penumbra_genesis::Content, - ) -> anyhow::Result> { + app_state: penumbra_app::genesis::Content, + ) -> anyhow::Result> { // Use now as genesis time let genesis_time = Time::from_unix_timestamp( SystemTime::now() @@ -272,7 +272,7 @@ impl TestnetConfig { }, // always empty in genesis json app_hash: tendermint::AppHash::default(), - app_state: penumbra_genesis::AppState::Content(app_state), + app_state: penumbra_app::genesis::AppState::Content(app_state), // Set empty validator set for Tendermint config, which falls back to reading // validators from the AppState, via ResponseInitChain: // https://docs.tendermint.com/v0.32/tendermint-core/using-tendermint.html @@ -282,12 +282,12 @@ impl TestnetConfig { } pub(crate) fn make_checkpoint( - genesis: Genesis, + genesis: Genesis, checkpoint: Option>, - ) -> Genesis { + ) -> Genesis { match checkpoint { Some(checkpoint) => Genesis { - app_state: penumbra_genesis::AppState::Checkpoint(checkpoint), + app_state: penumbra_app::genesis::AppState::Checkpoint(checkpoint), ..genesis }, None => genesis, @@ -672,7 +672,7 @@ mod tests { assert_eq!(testnet_config.name, "test-chain-1234"); assert_eq!(testnet_config.genesis.validators.len(), 0); // No external address template was given, so only 1 validator will be present. - let penumbra_genesis::AppState::Content(app_state) = testnet_config.genesis.app_state + let penumbra_app::genesis::AppState::Content(app_state) = testnet_config.genesis.app_state else { unimplemented!("TODO: support checkpointed app state") }; @@ -700,7 +700,7 @@ mod tests { )?; assert_eq!(testnet_config.name, "test-chain-4567"); assert_eq!(testnet_config.genesis.validators.len(), 0); - let penumbra_genesis::AppState::Content(app_state) = testnet_config.genesis.app_state + let penumbra_app::genesis::AppState::Content(app_state) = testnet_config.genesis.app_state else { unimplemented!("TODO: support checkpointed app state") }; diff --git a/crates/core/app/Cargo.toml b/crates/core/app/Cargo.toml index f44f8b12a6..8c04f1d87f 100644 --- a/crates/core/app/Cargo.toml +++ b/crates/core/app/Cargo.toml @@ -42,7 +42,6 @@ penumbra-dex = { workspace = true, default-features = true } penumbra-distributions = { workspace = true, default-features = true } penumbra-fee = { workspace = true, default-features = true } penumbra-funding = { workspace = true, default-features = true } -penumbra-genesis = { workspace = true } penumbra-governance = { workspace = true, default-features = true } penumbra-ibc = { workspace = true, features = ["component", "rpc"], default-features = true } penumbra-keys = { workspace = true, default-features = true } diff --git a/crates/core/app/src/app/mod.rs b/crates/core/app/src/app/mod.rs index e08991de43..afef02c339 100644 --- a/crates/core/app/src/app/mod.rs +++ b/crates/core/app/src/app/mod.rs @@ -15,7 +15,6 @@ use penumbra_distributions::component::{Distributions, StateReadExt as _, StateW use penumbra_fee::component::{Fee, StateReadExt as _, StateWriteExt as _}; use penumbra_funding::component::Funding; use penumbra_funding::component::{StateReadExt as _, StateWriteExt as _}; -use penumbra_genesis::AppState; use penumbra_governance::component::{Governance, StateReadExt as _}; use penumbra_governance::StateWriteExt as _; use penumbra_ibc::component::{Ibc, StateWriteExt as _}; @@ -39,6 +38,7 @@ use tendermint::validator::Update; use tracing::Instrument; use crate::action_handler::AppActionHandler; +use crate::genesis::AppState; use crate::params::AppParameters; use crate::{CommunityPoolStateReadExt, PenumbraHost}; diff --git a/crates/core/genesis/src/lib.rs b/crates/core/app/src/genesis.rs similarity index 100% rename from crates/core/genesis/src/lib.rs rename to crates/core/app/src/genesis.rs diff --git a/crates/core/app/src/lib.rs b/crates/core/app/src/lib.rs index 77cabcb848..a4cef23793 100644 --- a/crates/core/app/src/lib.rs +++ b/crates/core/app/src/lib.rs @@ -2,6 +2,7 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod app; +pub mod genesis; pub mod metrics; pub mod params; pub mod rpc; diff --git a/crates/core/app/src/server/consensus.rs b/crates/core/app/src/server/consensus.rs index 37fa3a35ef..ea42f474c7 100644 --- a/crates/core/app/src/server/consensus.rs +++ b/crates/core/app/src/server/consensus.rs @@ -117,7 +117,7 @@ impl Consensus { /// the database. async fn init_chain(&mut self, init_chain: request::InitChain) -> Result { // Note that errors cannot be handled in InitChain, the application must crash. - let app_state: penumbra_genesis::AppState = + let app_state: crate::genesis::AppState = serde_json::from_slice(&init_chain.app_state_bytes) .expect("can parse app_state in genesis file"); @@ -132,13 +132,13 @@ impl Consensus { let validators = self.app.tendermint_validator_updates(); let app_hash = match &app_state { - penumbra_genesis::AppState::Checkpoint(h) => { + crate::genesis::AppState::Checkpoint(h) => { tracing::info!(?h, "genesis state is a checkpoint"); // If we're starting from a checkpoint, we just need to forward the app hash // back to CometBFT. self.storage.latest_snapshot().root_hash().await? } - penumbra_genesis::AppState::Content(_) => { + crate::genesis::AppState::Content(_) => { tracing::info!("genesis state is a full configuration"); // Check that we haven't got a duplicated InitChain message for some reason: if self.storage.latest_version() != u64::MAX { diff --git a/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs index 7c94a4d794..25f6cdb4a9 100644 --- a/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs +++ b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs @@ -5,8 +5,7 @@ use { anyhow::{anyhow, Context}, cnidarium::TempStorage, decaf377_rdsa::{SigningKey, SpendAuth}, - penumbra_app::server::consensus::Consensus, - penumbra_genesis::AppState, + penumbra_app::{genesis::AppState, server::consensus::Consensus}, penumbra_keys::test_keys, penumbra_mock_client::MockClient, penumbra_mock_consensus::TestNode, @@ -32,7 +31,7 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { let storage = TempStorage::new().await?; // Configure an AppState with slightly shorter epochs than usual. - let app_state = AppState::Content(penumbra_genesis::Content { + let app_state = AppState::Content(penumbra_app::genesis::Content { sct_content: penumbra_sct::genesis::Content { sct_params: penumbra_sct::params::SctParameters { epoch_duration: EPOCH_DURATION, diff --git a/crates/core/app/tests/app_can_undelegate_from_a_validator.rs b/crates/core/app/tests/app_can_undelegate_from_a_validator.rs index f4eb0c8aee..de61fd1132 100644 --- a/crates/core/app/tests/app_can_undelegate_from_a_validator.rs +++ b/crates/core/app/tests/app_can_undelegate_from_a_validator.rs @@ -7,8 +7,7 @@ use { anyhow::anyhow, ark_ff::UniformRand, cnidarium::TempStorage, - penumbra_app::server::consensus::Consensus, - penumbra_genesis::AppState, + penumbra_app::{genesis::AppState, server::consensus::Consensus}, penumbra_keys::test_keys, penumbra_mock_client::MockClient, penumbra_mock_consensus::TestNode, @@ -59,7 +58,7 @@ async fn app_can_undelegate_from_a_validator() -> anyhow::Result<()> { }; // Configure an AppState with slightly shorter epochs than usual. - let app_state = AppState::Content(penumbra_genesis::Content { + let app_state = AppState::Content(penumbra_app::genesis::Content { sct_content: penumbra_sct::genesis::Content { sct_params: penumbra_sct::params::SctParameters { epoch_duration: EPOCH_DURATION, diff --git a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs index 711e79ea17..e5a76d2e41 100644 --- a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs +++ b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs @@ -4,8 +4,7 @@ use { self::common::BuilderExt, anyhow::Context, cnidarium::TempStorage, - penumbra_app::server::consensus::Consensus, - penumbra_genesis::AppState, + penumbra_app::{genesis::AppState, server::consensus::Consensus}, penumbra_mock_consensus::TestNode, penumbra_stake::{ component::validator_handler::validator_store::ValidatorDataRead, validator::Validator, diff --git a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs index da23d60847..7d0e3ba9c4 100644 --- a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs +++ b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs @@ -4,8 +4,7 @@ use { self::common::BuilderExt, anyhow::Context, cnidarium::TempStorage, - penumbra_app::server::consensus::Consensus, - penumbra_genesis::AppState, + penumbra_app::{genesis::AppState, server::consensus::Consensus}, penumbra_mock_consensus::TestNode, penumbra_stake::{ component::validator_handler::validator_store::ValidatorDataRead, validator::Validator, diff --git a/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs b/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs index 39656ddbff..70b2bdc041 100644 --- a/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs +++ b/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs @@ -4,8 +4,7 @@ use { self::common::BuilderExt, cnidarium::TempStorage, decaf377_rdsa::{SigningKey, SpendAuth}, - penumbra_app::server::consensus::Consensus, - penumbra_genesis::AppState, + penumbra_app::{genesis::AppState, server::consensus::Consensus}, penumbra_keys::test_keys, penumbra_mock_client::MockClient, penumbra_mock_consensus::TestNode, @@ -32,7 +31,7 @@ async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<( let storage = TempStorage::new().await?; // Configure an AppState with slightly shorter epochs than usual. - let app_state = AppState::Content(penumbra_genesis::Content { + let app_state = AppState::Content(penumbra_app::genesis::Content { sct_content: penumbra_sct::genesis::Content { sct_params: penumbra_sct::params::SctParameters { epoch_duration: EPOCH_DURATION, diff --git a/crates/core/app/tests/common/mod.rs b/crates/core/app/tests/common/mod.rs index 3df34db4c1..38f9af1d74 100644 --- a/crates/core/app/tests/common/mod.rs +++ b/crates/core/app/tests/common/mod.rs @@ -10,9 +10,9 @@ use { cnidarium::TempStorage, penumbra_app::{ app::App, + genesis::AppState, server::consensus::{Consensus, ConsensusService}, }, - penumbra_genesis::AppState, penumbra_mock_consensus::TestNode, std::ops::Deref, }; diff --git a/crates/core/app/tests/common/test_node_builder_ext.rs b/crates/core/app/tests/common/test_node_builder_ext.rs index dfb4ca0e74..6362e6343d 100644 --- a/crates/core/app/tests/common/test_node_builder_ext.rs +++ b/crates/core/app/tests/common/test_node_builder_ext.rs @@ -1,5 +1,5 @@ use { - penumbra_genesis::AppState, + penumbra_app::genesis::AppState, penumbra_mock_consensus::builder::Builder, penumbra_proto::{ core::keys::v1::{GovernanceKey, IdentityKey}, diff --git a/crates/core/genesis/Cargo.toml b/crates/core/genesis/Cargo.toml deleted file mode 100644 index e1af2730e8..0000000000 --- a/crates/core/genesis/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "penumbra-genesis" -authors.workspace = true -edition.workspace = true -version.workspace = true -repository.workspace = true -homepage.workspace = true -license.workspace = true - -[dependencies] -anyhow = { workspace = true } -penumbra-community-pool = { workspace = true } -penumbra-distributions = { workspace = true } -penumbra-fee = { workspace = true } -penumbra-dex = { workspace = true } -penumbra-funding = { workspace = true } -penumbra-governance = { workspace = true } -penumbra-ibc = { workspace = true } -penumbra-proto = { workspace = true } -penumbra-sct = { workspace = true } -penumbra-shielded-pool = { workspace = true } -penumbra-stake = { workspace = true } -serde = { workspace = true, features = ["derive"] } diff --git a/deployments/scripts/rust-docs b/deployments/scripts/rust-docs index e8d0330b9c..1eaad2ebc5 100755 --- a/deployments/scripts/rust-docs +++ b/deployments/scripts/rust-docs @@ -38,7 +38,6 @@ cargo +nightly doc --no-deps \ -p penumbra-dex \ -p penumbra-distributions \ -p penumbra-fee \ - -p penumbra-genesis \ -p penumbra-governance \ -p penumbra-ibc \ -p penumbra-keys \ From d012f17f63391f942be293a9e66d383fa3191673 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Mon, 1 Apr 2024 18:29:59 -0400 Subject: [PATCH 060/168] dex: fix incorrect nomenclature (#4143) --- .../pcli/src/command/tx/liquidity_position.rs | 16 +-- .../src/component/circuit_breaker/value.rs | 6 +- .../component/dex/src/component/router/mod.rs | 2 +- .../dex/src/component/router/tests.rs | 120 +++++++++--------- .../core/component/dex/src/component/tests.rs | 28 ++-- 5 files changed, 86 insertions(+), 86 deletions(-) diff --git a/crates/bin/pcli/src/command/tx/liquidity_position.rs b/crates/bin/pcli/src/command/tx/liquidity_position.rs index 12981d4af4..9eb744dd3b 100644 --- a/crates/bin/pcli/src/command/tx/liquidity_position.rs +++ b/crates/bin/pcli/src/command/tx/liquidity_position.rs @@ -98,9 +98,9 @@ pub enum OrderCmd { /// Only spend funds originally received by the given address index. #[clap(long, default_value = "0")] source: u32, - /// When set, tags the position as being a limit-sell order. + /// When set, tags the position as an auto-closing buy. #[clap(long)] - limit_order: bool, + auto_close: bool, /// The selected fee tier to multiply the fee amount by. #[clap(short, long, value_enum, default_value_t)] fee_tier: FeeTier, @@ -115,9 +115,9 @@ pub enum OrderCmd { /// Only spend funds originally received by the given address index. #[clap(long, default_value = "0")] source: u32, - /// When set, tags the position as being a limit-sell order. + /// When set, tags the position as an auto-closing sell. #[clap(long)] - limit_order: bool, + auto_close: bool, /// The selected fee tier to multiply the fee amount by. #[clap(short, long, value_enum, default_value_t)] fee_tier: FeeTier, @@ -139,10 +139,10 @@ impl OrderCmd { } } - pub fn limit_order(&self) -> bool { + pub fn is_auto_closing(&self) -> bool { match self { - OrderCmd::Buy { limit_order, .. } => *limit_order, - OrderCmd::Sell { limit_order, .. } => *limit_order, + OrderCmd::Buy { auto_close, .. } => *auto_close, + OrderCmd::Sell { auto_close, .. } => *auto_close, } } @@ -166,7 +166,7 @@ impl OrderCmd { }; tracing::info!(?position); - if self.limit_order() { + if self.is_auto_closing() { position.close_on_fill = true; } diff --git a/crates/core/component/dex/src/component/circuit_breaker/value.rs b/crates/core/component/dex/src/component/circuit_breaker/value.rs index ac76907569..8f4b6396b2 100644 --- a/crates/core/component/dex/src/component/circuit_breaker/value.rs +++ b/crates/core/component/dex/src/component/circuit_breaker/value.rs @@ -52,7 +52,7 @@ mod tests { use crate::component::{StateReadExt as _, StateWriteExt as _}; use crate::lp::plan::PositionWithdrawPlan; use crate::{ - component::{router::limit_buy, tests::TempStorageExt}, + component::{router::create_buy, tests::TempStorageExt}, state_key, DirectedUnitPair, }; use crate::{BatchSwapOutputData, PositionOpen}; @@ -132,7 +132,7 @@ mod tests { let one = 1u64.into(); let price1 = one; // Create a position buying 1 gm with 1 gn (i.e. reserves will be 1gn). - let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); + let buy_1 = create_buy(pair_1.clone(), 1u64.into(), price1); // Create the PositionOpen action let pos_open = PositionOpen { @@ -220,7 +220,7 @@ mod tests { let one = 1u64.into(); let price1 = one; // Create a position buying 1 gm with 1 gn (i.e. reserves will be 1gn). - let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); + let buy_1 = create_buy(pair_1.clone(), 1u64.into(), price1); let id = buy_1.id(); diff --git a/crates/core/component/dex/src/component/router/mod.rs b/crates/core/component/dex/src/component/router/mod.rs index 0a01488982..732d590ba7 100644 --- a/crates/core/component/dex/src/component/router/mod.rs +++ b/crates/core/component/dex/src/component/router/mod.rs @@ -17,4 +17,4 @@ pub use route_and_fill::{HandleBatchSwaps, RouteAndFill}; mod tests; #[cfg(test)] -pub(crate) use tests::{limit_buy, limit_sell}; +pub(crate) use tests::{create_buy, create_sell}; diff --git a/crates/core/component/dex/src/component/router/tests.rs b/crates/core/component/dex/src/component/router/tests.rs index bd9c5f4de6..ccb530b738 100644 --- a/crates/core/component/dex/src/component/router/tests.rs +++ b/crates/core/component/dex/src/component/router/tests.rs @@ -339,7 +339,7 @@ async fn create_test_positions_basic(s: &mut S, misprice: bool) { /// Create a `Position` to buy `asset_1` using `asset_2` with explicit p/q. /// e.g. "Buy `quantity` of `asset_1` for `price` units of `asset_2` each. -fn limit_buy_pq( +fn create_buy_pq( market: DirectedUnitPair, quantity: Amount, p: Amount, @@ -361,7 +361,7 @@ fn limit_buy_pq( /// Create a `Position` to buy `asset_1` using `asset_2`. /// e.g. "Buy `quantity` of `asset_1` for `price` units of `asset_2` each. -pub(crate) fn limit_buy( +pub(crate) fn create_buy( market: DirectedUnitPair, quantity: Amount, price_in_numeraire: Amount, @@ -380,7 +380,7 @@ pub(crate) fn limit_buy( } /// Create a `Position` to sell `asset_1` into `asset_2`. -pub(crate) fn limit_sell( +pub(crate) fn create_sell( market: DirectedUnitPair, quantity: Amount, price_in_numeraire: Amount, @@ -489,8 +489,8 @@ async fn test_multiple_similar_position() -> anyhow::Result<()> { let one = 1u64.into(); let price1 = one; - let mut buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); - let mut buy_2 = limit_buy(pair_1.clone(), 1u64.into(), price1); + let mut buy_1 = create_buy(pair_1.clone(), 1u64.into(), price1); + let mut buy_2 = create_buy(pair_1.clone(), 1u64.into(), price1); buy_1.nonce = [1u8; 32]; buy_2.nonce = [2u8; 32]; state_tx.open_position(buy_1.clone()).await.unwrap(); @@ -569,18 +569,18 @@ async fn fill_route_constraint_stacked() -> anyhow::Result<()> { let price1 = one; let price2 = 2u64.into(); - let buy_1 = limit_buy(pair_1.clone(), 3u64.into(), price2); - let buy_2 = limit_buy(pair_1.clone(), 1u64.into(), price1); + let buy_1 = create_buy(pair_1.clone(), 3u64.into(), price2); + let buy_2 = create_buy(pair_1.clone(), 1u64.into(), price1); state_tx.open_position(buy_1).await.unwrap(); state_tx.open_position(buy_2).await.unwrap(); /* pair 2 */ let price2 = Amount::from(2u64); - let buy_1 = limit_buy(pair_2.clone(), 1u64.into(), price2); - let buy_2 = limit_buy(pair_2.clone(), 50u64.into(), price1); - let buy_3 = limit_buy(pair_2.clone(), 50u64.into(), price1); - let buy_4 = limit_buy(pair_2.clone(), 50u64.into(), price1); + let buy_1 = create_buy(pair_2.clone(), 1u64.into(), price2); + let buy_2 = create_buy(pair_2.clone(), 50u64.into(), price1); + let buy_3 = create_buy(pair_2.clone(), 50u64.into(), price1); + let buy_4 = create_buy(pair_2.clone(), 50u64.into(), price1); state_tx.open_position(buy_1).await.unwrap(); state_tx.open_position(buy_2).await.unwrap(); @@ -594,11 +594,11 @@ async fn fill_route_constraint_stacked() -> anyhow::Result<()> { let price3100 = 3100u64.into(); let price10000 = 10_000u64.into(); - let buy_1 = limit_buy(pair_3.clone(), 1u64.into(), price2000); - let buy_2 = limit_buy(pair_3.clone(), 1u64.into(), price2500); - let buy_3 = limit_buy(pair_3.clone(), 198u64.into(), price3000); - let buy_4 = limit_buy(pair_3.clone(), 1u64.into(), price3100); - let buy_5 = limit_buy(pair_3.clone(), 1u64.into(), price10000); + let buy_1 = create_buy(pair_3.clone(), 1u64.into(), price2000); + let buy_2 = create_buy(pair_3.clone(), 1u64.into(), price2500); + let buy_3 = create_buy(pair_3.clone(), 198u64.into(), price3000); + let buy_4 = create_buy(pair_3.clone(), 1u64.into(), price3100); + let buy_5 = create_buy(pair_3.clone(), 1u64.into(), price10000); state_tx.open_position(buy_1).await.unwrap(); state_tx.open_position(buy_2).await.unwrap(); @@ -680,16 +680,16 @@ async fn fill_route_constraint_1() -> anyhow::Result<()> { let price1 = one; - let buy_1 = limit_buy(pair_1.clone(), 200u64.into(), price1); + let buy_1 = create_buy(pair_1.clone(), 200u64.into(), price1); state_tx.open_position(buy_1).await.unwrap(); /* pair 2 */ let price2 = Amount::from(2u64); - let buy_1 = limit_buy(pair_2.clone(), 50u64.into(), price2); - let buy_2 = limit_buy(pair_2.clone(), 50u64.into(), price2); - let buy_3 = limit_buy(pair_2.clone(), 50u64.into(), price2); - let buy_4 = limit_buy(pair_2.clone(), 50u64.into(), price2); + let buy_1 = create_buy(pair_2.clone(), 50u64.into(), price2); + let buy_2 = create_buy(pair_2.clone(), 50u64.into(), price2); + let buy_3 = create_buy(pair_2.clone(), 50u64.into(), price2); + let buy_4 = create_buy(pair_2.clone(), 50u64.into(), price2); state_tx.open_position(buy_1).await.unwrap(); state_tx.open_position(buy_2).await.unwrap(); @@ -703,11 +703,11 @@ async fn fill_route_constraint_1() -> anyhow::Result<()> { let price3100 = 3100u64.into(); let price10000 = 10_000u64.into(); - let buy_1 = limit_buy(pair_3.clone(), 1u64.into(), price2000); - let buy_2 = limit_buy(pair_3.clone(), 1u64.into(), price2500); - let buy_3 = limit_buy(pair_3.clone(), 198u64.into(), price3000); - let buy_4 = limit_buy(pair_3.clone(), 1u64.into(), price3100); - let buy_5 = limit_buy(pair_3.clone(), 1u64.into(), price10000); + let buy_1 = create_buy(pair_3.clone(), 1u64.into(), price2000); + let buy_2 = create_buy(pair_3.clone(), 1u64.into(), price2500); + let buy_3 = create_buy(pair_3.clone(), 198u64.into(), price3000); + let buy_4 = create_buy(pair_3.clone(), 1u64.into(), price3100); + let buy_5 = create_buy(pair_3.clone(), 1u64.into(), price10000); state_tx.open_position(buy_1).await.unwrap(); state_tx.open_position(buy_2).await.unwrap(); @@ -783,22 +783,22 @@ async fn fill_route_unconstrained() -> anyhow::Result<()> { let one = 1u64.into(); let price1 = one; - let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); - let buy_2 = limit_buy(pair_1.clone(), 1u64.into(), price1); + let buy_1 = create_buy(pair_1.clone(), 1u64.into(), price1); + let buy_2 = create_buy(pair_1.clone(), 1u64.into(), price1); state_tx.open_position(buy_1).await.unwrap(); state_tx.open_position(buy_2).await.unwrap(); - let buy_1 = limit_buy(pair_2.clone(), 1u64.into(), price1); - let buy_2 = limit_buy(pair_2.clone(), 1u64.into(), price1); + let buy_1 = create_buy(pair_2.clone(), 1u64.into(), price1); + let buy_2 = create_buy(pair_2.clone(), 1u64.into(), price1); state_tx.open_position(buy_1).await.unwrap(); state_tx.open_position(buy_2).await.unwrap(); let price1500 = 1500u64.into(); - let buy_1 = limit_buy(pair_3.clone(), 1u64.into(), price1500); - let buy_2 = limit_buy(pair_3.clone(), 1u64.into(), price1500); - let buy_3 = limit_buy(pair_3.clone(), 1u64.into(), price1500); - let buy_4 = limit_buy(pair_3.clone(), 1u64.into(), price1500); - let buy_5 = limit_buy(pair_3.clone(), 1u64.into(), price1500); + let buy_1 = create_buy(pair_3.clone(), 1u64.into(), price1500); + let buy_2 = create_buy(pair_3.clone(), 1u64.into(), price1500); + let buy_3 = create_buy(pair_3.clone(), 1u64.into(), price1500); + let buy_4 = create_buy(pair_3.clone(), 1u64.into(), price1500); + let buy_5 = create_buy(pair_3.clone(), 1u64.into(), price1500); state_tx.open_position(buy_1).await.unwrap(); state_tx.open_position(buy_2).await.unwrap(); state_tx.open_position(buy_3).await.unwrap(); @@ -877,14 +877,14 @@ async fn fill_route_hit_spill_price() -> anyhow::Result<()> { let one = 1u64.into(); let price1 = one; - let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); - let buy_2 = limit_buy(pair_1.clone(), 2u64.into(), price1); + let buy_1 = create_buy(pair_1.clone(), 1u64.into(), price1); + let buy_2 = create_buy(pair_1.clone(), 2u64.into(), price1); state_tx.open_position(buy_1).await.unwrap(); state_tx.open_position(buy_2).await.unwrap(); - let buy_1 = limit_buy(pair_2.clone(), one, price1); - let buy_2 = limit_buy(pair_2.clone(), one, price1); - let buy_3 = limit_buy(pair_2.clone(), one, price1); + let buy_1 = create_buy(pair_2.clone(), one, price1); + let buy_2 = create_buy(pair_2.clone(), one, price1); + let buy_3 = create_buy(pair_2.clone(), one, price1); state_tx.open_position(buy_1).await.unwrap(); state_tx.open_position(buy_2).await.unwrap(); state_tx.open_position(buy_3).await.unwrap(); @@ -893,9 +893,9 @@ async fn fill_route_hit_spill_price() -> anyhow::Result<()> { let price1400 = Amount::from(1400u64); let price1300 = Amount::from(1300u64); - let buy_1 = limit_buy(pair_3.clone(), one, price1500); - let buy_2 = limit_buy(pair_3.clone(), one, price1400); - let buy_3 = limit_buy(pair_3.clone(), one, price1300); + let buy_1 = create_buy(pair_3.clone(), one, price1500); + let buy_2 = create_buy(pair_3.clone(), one, price1400); + let buy_3 = create_buy(pair_3.clone(), one, price1300); state_tx.open_position(buy_1).await.unwrap(); state_tx.open_position(buy_2).await.unwrap(); state_tx.open_position(buy_3).await.unwrap(); @@ -955,7 +955,7 @@ async fn simple_route() -> anyhow::Result<()> { let pair_1 = DirectedUnitPair::new(gn.clone(), penumbra.clone()); // Create a single 1:1 gn:penumbra position (i.e. buy 1 gn at 1 penumbra). - let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), 1u64.into()); + let buy_1 = create_buy(pair_1.clone(), 1u64.into(), 1u64.into()); state_tx.open_position(buy_1).await.unwrap(); state_tx.apply(); @@ -990,7 +990,7 @@ async fn best_position_route_and_fill() -> anyhow::Result<()> { let pair_1 = DirectedUnitPair::new(gn.clone(), penumbra.clone()); // Create a single 1:1 gn:penumbra position (i.e. buy 1 gn at 1 penumbra). - let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), 1u64.into()); + let buy_1 = create_buy(pair_1.clone(), 1u64.into(), 1u64.into()); state_tx.open_position(buy_1).await.unwrap(); state_tx.apply(); @@ -1068,7 +1068,7 @@ async fn multi_hop_route_and_fill() -> anyhow::Result<()> { let pair_gm_penumbra = DirectedUnitPair::new(gm.clone(), penumbra.clone()); // Create a 2:1 penumbra:gm position (i.e. buy 20 gm at 2 penumbra each). - let buy_1 = limit_buy_pq( + let buy_1 = create_buy_pq( pair_gm_penumbra.clone(), 5u64.into(), 1u64.into(), @@ -1078,7 +1078,7 @@ async fn multi_hop_route_and_fill() -> anyhow::Result<()> { state_tx.open_position(buy_1).await.unwrap(); // Create a 2.1:1 penumbra:gm position (i.e. buy 40 gm at 2.1 penumbra each). - let buy_2 = limit_buy_pq( + let buy_2 = create_buy_pq( pair_gm_penumbra.clone(), 40u64.into(), 1000000u64.into(), @@ -1088,7 +1088,7 @@ async fn multi_hop_route_and_fill() -> anyhow::Result<()> { state_tx.open_position(buy_2).await.unwrap(); // Create a 2.2:1 penumbra:gm position (i.e. buy 160 gm at 2.2 penumbra each). - let buy_3 = limit_buy_pq( + let buy_3 = create_buy_pq( pair_gm_penumbra.clone(), 160u64.into(), 1000000u64.into(), @@ -1098,7 +1098,7 @@ async fn multi_hop_route_and_fill() -> anyhow::Result<()> { state_tx.open_position(buy_3).await.unwrap(); // Create a 1:1 gm:gn position (i.e. buy 100 gm at 1 gn each). - let buy_4 = limit_buy_pq( + let buy_4 = create_buy_pq( pair_gm_gn.clone(), 100u64.into(), 1u64.into(), @@ -1123,7 +1123,7 @@ async fn multi_hop_route_and_fill() -> anyhow::Result<()> { state_tx.open_position(buy_5).await.unwrap(); // Create a 1:1 gm:gn position (i.e. buy 100 gn at 1 gm each). - let buy_6 = limit_buy_pq( + let buy_6 = create_buy_pq( pair_gn_gm.clone(), 100u64.into(), 1u64.into(), @@ -1217,8 +1217,8 @@ async fn fill_dust_route() -> anyhow::Result<()> { let one = 1u64.into(); let price1 = one; - let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); - let buy_2 = limit_buy(pair_1.clone(), 1u64.into(), price1); + let buy_1 = create_buy(pair_1.clone(), 1u64.into(), price1); + let buy_2 = create_buy(pair_1.clone(), 1u64.into(), price1); state_tx.open_position(buy_1).await.unwrap(); state_tx.open_position(buy_2).await.unwrap(); let dust_constraint = Position::new( @@ -1282,8 +1282,8 @@ async fn fill_route_dust() { let one = 1u64.into(); let price1 = one; - let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); - let buy_2 = limit_buy(pair_1.clone(), 1u64.into(), price1); + let buy_1 = create_buy(pair_1.clone(), 1u64.into(), price1); + let buy_2 = create_buy(pair_1.clone(), 1u64.into(), price1); state_tx.open_position(buy_1).await.unwrap(); state_tx.open_position(buy_2).await.unwrap(); let dust_constraint = Position::new( @@ -1347,8 +1347,8 @@ async fn fill_route_with_dust_constraint() -> anyhow::Result<()> { let one = 1u64.into(); let price1 = one; - let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); - let buy_2 = limit_buy(pair_1.clone(), 1u64.into(), price1); + let buy_1 = create_buy(pair_1.clone(), 1u64.into(), price1); + let buy_2 = create_buy(pair_1.clone(), 1u64.into(), price1); state_tx.open_position(buy_1).await.unwrap(); state_tx.open_position(buy_2).await.unwrap(); @@ -1378,7 +1378,7 @@ async fn fill_route_with_dust_constraint() -> anyhow::Result<()> { state_tx.open_position(dust_constraint).await.unwrap(); state_tx.open_position(normal_order).await.unwrap(); - let buy_1 = limit_buy(pair_3, 100u64.into(), 1400u64.into()); + let buy_1 = create_buy(pair_3, 100u64.into(), 1400u64.into()); state_tx.open_position(buy_1).await.unwrap(); let delta_1 = Value { @@ -1435,8 +1435,8 @@ async fn fill_route_with_stacked_dust_constraint() -> anyhow::Result<()> { let one = 1u64.into(); let price1 = one; - let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); - let buy_2 = limit_buy(pair_1.clone(), 1u64.into(), price1); + let buy_1 = create_buy(pair_1.clone(), 1u64.into(), price1); + let buy_2 = create_buy(pair_1.clone(), 1u64.into(), price1); state_tx.open_position(buy_1).await.unwrap(); state_tx.open_position(buy_2).await.unwrap(); @@ -1494,7 +1494,7 @@ async fn fill_route_with_stacked_dust_constraint() -> anyhow::Result<()> { state_tx.open_position(dust_constraint_p3).await.unwrap(); state_tx.open_position(normal_order_p3).await.unwrap(); - let buy_1 = limit_buy(pair_4, 100u64.into(), 1400u64.into()); + let buy_1 = create_buy(pair_4, 100u64.into(), 1400u64.into()); state_tx.open_position(buy_1).await.unwrap(); let delta_1 = Value { diff --git a/crates/core/component/dex/src/component/tests.rs b/crates/core/component/dex/src/component/tests.rs index 69baee6c9b..b910c0998b 100644 --- a/crates/core/component/dex/src/component/tests.rs +++ b/crates/core/component/dex/src/component/tests.rs @@ -14,7 +14,7 @@ use crate::DexParameters; use crate::{ component::{ router::FillRoute, - router::{limit_buy, limit_sell, HandleBatchSwaps, RoutingParams}, + router::{create_buy, create_sell, HandleBatchSwaps, RoutingParams}, Arbitrage, PositionManager, PositionRead, StateReadExt, StateWriteExt, }, lp::{position::Position, Reserves}, @@ -51,7 +51,7 @@ impl TempStorageExt for TempStorage { #[tokio::test] /// Builds a simple order book with a single limit order, and tests different /// market order execution against it. -async fn single_limit_order() -> anyhow::Result<()> { +async fn single_close_on_fill() -> anyhow::Result<()> { let storage = TempStorage::new().await?.apply_minimal_genesis().await?; let mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); let mut state_tx = state.try_begin_transaction().unwrap(); @@ -314,7 +314,7 @@ async fn check_close_on_fill() -> anyhow::Result<()> { #[tokio::test] /// Try to execute against multiple positions, mainly testing that the order-book traversal /// is done correctly. -async fn multiple_limit_orders() -> anyhow::Result<()> { +async fn multiple_close_on_fills() -> anyhow::Result<()> { let storage = TempStorage::new().await?.apply_minimal_genesis().await?; let mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); let mut state_tx = state.try_begin_transaction().unwrap(); @@ -609,7 +609,7 @@ async fn swap_execution_tests() -> anyhow::Result<()> { let pair_gn_penumbra = DirectedUnitPair::new(gn.clone(), penumbra.clone()); // Create a single 1:1 gn:penumbra position (i.e. buy 1 gn at 1 penumbra). - let buy_1 = limit_buy(pair_gn_penumbra.clone(), 1u64.into(), 1u64.into()); + let buy_1 = create_buy(pair_gn_penumbra.clone(), 1u64.into(), 1u64.into()); state_tx.open_position(buy_1).await.unwrap(); state_tx.apply(); @@ -684,7 +684,7 @@ async fn swap_execution_tests() -> anyhow::Result<()> { // Sell 25 gn at 1 gm each. state_tx - .open_position(limit_sell( + .open_position(create_sell( DirectedUnitPair::new(gn.clone(), gm.clone()), 25u64.into(), 1u64.into(), @@ -693,7 +693,7 @@ async fn swap_execution_tests() -> anyhow::Result<()> { .unwrap(); // Buy 1 pusd at 20 gm each. state_tx - .open_position(limit_buy( + .open_position(create_buy( DirectedUnitPair::new(pusd.clone(), gm.clone()), 1u64.into(), 20u64.into(), @@ -702,7 +702,7 @@ async fn swap_execution_tests() -> anyhow::Result<()> { .unwrap(); // Buy 5 penumbra at 1 gm each. state_tx - .open_position(limit_buy( + .open_position(create_buy( DirectedUnitPair::new(penumbra.clone(), gm.clone()), 5u64.into(), 1u64.into(), @@ -711,7 +711,7 @@ async fn swap_execution_tests() -> anyhow::Result<()> { .unwrap(); // Sell 1pusd at 5 penumbra each. state_tx - .open_position(limit_sell( + .open_position(create_sell( DirectedUnitPair::new(pusd.clone(), penumbra.clone()), 1u64.into(), 5u64.into(), @@ -812,7 +812,7 @@ async fn basic_cycle_arb() -> anyhow::Result<()> { // Sell 10 gn at 1 penumbra each. state_tx - .open_position(limit_sell( + .open_position(create_sell( DirectedUnitPair::new(gn.clone(), penumbra.clone()), 10u64.into(), 1u64.into(), @@ -821,7 +821,7 @@ async fn basic_cycle_arb() -> anyhow::Result<()> { .unwrap(); // Buy 100 gn at 2 gm each. state_tx - .open_position(limit_buy( + .open_position(create_buy( DirectedUnitPair::new(gn.clone(), gm.clone()), 100u64.into(), 2u64.into(), @@ -830,7 +830,7 @@ async fn basic_cycle_arb() -> anyhow::Result<()> { .unwrap(); // Sell 100 penumbra at 1 gm each. state_tx - .open_position(limit_sell( + .open_position(create_sell( DirectedUnitPair::new(penumbra.clone(), gm.clone()), 100u64.into(), 1u64.into(), @@ -914,13 +914,13 @@ async fn reproduce_arbitrage_loop_testnet_53() -> anyhow::Result<()> { * */ - let mut buy_1 = limit_buy(penumbra_usd.clone(), 1u64.into(), 110u64.into()); + let mut buy_1 = create_buy(penumbra_usd.clone(), 1u64.into(), 110u64.into()); buy_1.nonce = [1; 32]; - let mut buy_2 = limit_buy(penumbra_usd.clone(), 1u64.into(), 100u64.into()); + let mut buy_2 = create_buy(penumbra_usd.clone(), 1u64.into(), 100u64.into()); buy_2.nonce = [2; 32]; - let mut sell_1 = limit_sell(penumbra_usd.clone(), 10u64.into(), 100u64.into()); + let mut sell_1 = create_sell(penumbra_usd.clone(), 10u64.into(), 100u64.into()); sell_1.nonce = [0; 32]; state_tx.open_position(buy_1).await.unwrap(); From c0b4ba6d401afab30b9643b41782539db0e23519 Mon Sep 17 00:00:00 2001 From: aubrey Date: Tue, 26 Mar 2024 17:39:50 -0700 Subject: [PATCH 061/168] fees: add per-action block space calculations & representative values, fix proto comment on OutputBody size --- .../src/gen/proto_descriptor.bin.no_lfs | Bin 98383 -> 88631 bytes crates/core/transaction/src/gas.rs | 168 ++++++++++-------- ...enumbra.core.component.shielded_pool.v1.rs | 2 +- .../shielded_pool/v1/shielded_pool.proto | 2 +- 4 files changed, 99 insertions(+), 73 deletions(-) diff --git a/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs b/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs index 67d781308dc15349cc1633e0986b9be4d49fda1f..c2c1f4df906c5e549e4bf4ff1514fc587b551e16 100644 GIT binary patch delta 16472 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 WQ+w4Mx1hSdx_-eQmpW|U1OEvTzRX+z delta 26297 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 diff --git a/crates/core/transaction/src/gas.rs b/crates/core/transaction/src/gas.rs index 76927d753e..f011698b74 100644 --- a/crates/core/transaction/src/gas.rs +++ b/crates/core/transaction/src/gas.rs @@ -16,6 +16,8 @@ use crate::{ Action, Transaction, }; +use penumbra_proto::DomainType; + const NULLIFIER_SIZE: u64 = 2 + 32; const NOTEPAYLOAD_SIZE: u64 = 2 + 32 + 2 + 32 + 2 + 132; const SWAPPAYLOAD_SIZE: u64 = 2 + 32 + 2 + 272; @@ -29,11 +31,19 @@ pub trait GasCost { fn gas_cost(&self) -> Gas; } +// Where block space costs are hard-coded instead of calculated in the following functions, the values are based on the approximate byte size of the +// encoded action and ignore the protobuf framing overhead, because it makes only a small difference and simplifies accounting. + pub fn spend_gas_cost() -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // Of fixed size, so we hardcode the block space cost proportional to the size of the following fields of the protobuf encoding of the type: + + // penumbra.core.asset.v1.BalanceCommitment = 32 bytes + // penumbra.core.component.sct.v1.Nullifier = 32 bytes + // penumbra.crypto.decaf377_rdsa.v1.SpendVerificationKey = 32 bytes + // penumbra.crypto.decaf377_rdsa.v1.SpendAuthSignature = 64 bytes + // ZKSpendProof = 192 bytes + block_space: 352, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. // For a Spend this is the byte size of a `Nullifier`. @@ -47,9 +57,13 @@ pub fn spend_gas_cost() -> Gas { pub fn output_gas_cost() -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // Of fixed size, so we hardcode the block space cost proportional to the size of the following fields of the protobuf encoding of the type: + // NOTEPAYLOAD_SIZE = 202 bytes + // penumbra.core.asset.v1.BalanceCommitment = 32 bytes + // wrapped_memo_key = 48 bytes + // ovk_wrapped_key = 48 bytes + // ZKOutputProof = 192 bytes + block_space: NOTEPAYLOAD_SIZE + 352, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. compact_block_space: NOTEPAYLOAD_SIZE, @@ -60,11 +74,10 @@ pub fn output_gas_cost() -> Gas { } } -fn delegate_gas_cost() -> Gas { +fn delegate_gas_cost(delegate: &Delegate) -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // The block space measured as the byte length of the encoded action. + block_space: delegate.encode_to_vec().len() as u64, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. // For a Delegate, nothing is added to the compact block directly. The associated [`Action::Spend`] @@ -77,11 +90,10 @@ fn delegate_gas_cost() -> Gas { } } -fn undelegate_gas_cost() -> Gas { +fn undelegate_gas_cost(undelegate: &Undelegate) -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // The block space measured as the byte length of the encoded action. + block_space: undelegate.encode_to_vec().len() as u64, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. // For an Undelegate, nothing is added to the compact block directly. The associated [`Action::Spend`] @@ -96,9 +108,13 @@ fn undelegate_gas_cost() -> Gas { fn undelegate_claim_gas_cost() -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // penumbra.core.keys.v1.IdentityKey = 64 bytes + // uint64 = 8 bytes + // Penalty penalty = 64 bytes + // penumbra.core.asset.v1.BalanceCommitment = 32 bytes + // uint64 = 8 bytes + // proof = 192 bytes + block_space: 368, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. // For an UndelegateClaim, nothing is added to the compact block directly. The associated [`Action::Output`] @@ -111,11 +127,10 @@ fn undelegate_claim_gas_cost() -> Gas { } } -fn validator_definition_gas_cost() -> Gas { +fn validator_definition_gas_cost(validator_definition: &ValidatorDefinition) -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // The block space measured as the byte length of the encoded action. + block_space: validator_definition.encode_to_vec().len() as u64, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. // For a ValidatorDefinition the compact block is not modified. @@ -129,9 +144,14 @@ fn validator_definition_gas_cost() -> Gas { fn swap_gas_cost() -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // ZKSwapProof = 192 bytes + // TradingPair = 128 bytes + // penumbra.core.num.v1.Amount = 64 bytes + // penumbra.core.num.v1.Amount = 64 bytes + // penumbra.core.asset.v1.BalanceCommitment = 32 bytes + // SwapPayload payload = 308 bytes + // batch swap output data = 104 bytes + block_space: 192 + 128 + 64 + 64 + SWAPPAYLOAD_SIZE + BSOD_SIZE, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. // For a Swap this is the byte size of a [`StatePayload`] and a [`BatchSwapOutputData`]. @@ -149,9 +169,14 @@ fn swap_gas_cost() -> Gas { pub fn swap_claim_gas_cost() -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // ZKSwapClaimProof = 192 bytes + // penumbra.core.component.sct.v1.Nullifier = 32 bytes + // penumbra.core.component.fee.v1.Fee fee = 128 + 128 + 64 bytes + // penumbra.crypto.tct.v1.StateCommitment output_1_commitment = 64 bytes + // penumbra.crypto.tct.v1.StateCommitment output_2_commitment = 64 bytes + // BatchSwapOutputData output_data = 104 bytes + // uint64 epoch_duration = 8 bytes + block_space: 192 + 32 + 128 + 128 + 64 + 64 + 64 + BSOD_SIZE + 8, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. // For a SwapClaim, nothing is added to the compact block directly. The associated [`Action::Spend`] @@ -166,9 +191,18 @@ pub fn swap_claim_gas_cost() -> Gas { fn delegator_vote_gas_cost() -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // uint64 = 8 bytes + // uint64 = 8 bytes + // Vote vote = 8 bytes + // penumbra.core.asset.v1.Value = 8 + 8 + 64 + 64 + 64 bytes + // penumbra.core.num.v1.Amount unbonded_amount = 64 bytes + // penumbra.core.component.sct.v1.Nullifier nullifier = 32 bytes + // penumbra.crypto.decaf377_rdsa.v1.SpendVerificationKey rk = 64 bytes + // penumbra.crypto.decaf377_rdsa.v1.SpendAuthSignature auth_sig = 64 bytes + // ZKDelegatorVoteProof proof = 192 bytes + + // The block space measured as the byte length of the encoded action. + block_space: 8 + 8 + 8 + 8 + 8 + 64 + 64 + 64 + 64 + 32 + 64 + 64 + 192, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. // For a DelegatorVote the compact block is not modified. @@ -182,9 +216,10 @@ fn delegator_vote_gas_cost() -> Gas { fn position_withdraw_gas_cost() -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // position ID = 64 + 64 bytes + // balance commitment = 64 bytes + // uint64 = 8 bytes + block_space: 64 + 64 + 64 + 8, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. // For a PositionWithdraw the compact block is not modified. @@ -224,21 +259,22 @@ impl GasCost for ActionPlan { // and can call the `GasCost` impl on that. ActionPlan::Spend(_) => spend_gas_cost(), ActionPlan::Output(_) => output_gas_cost(), - ActionPlan::Delegate(d) => d.gas_cost(), - ActionPlan::Undelegate(u) => u.gas_cost(), ActionPlan::UndelegateClaim(_) => undelegate_claim_gas_cost(), - ActionPlan::ValidatorDefinition(vd) => vd.gas_cost(), ActionPlan::Swap(_) => swap_gas_cost(), ActionPlan::SwapClaim(_) => swap_claim_gas_cost(), + ActionPlan::DelegatorVote(_) => delegator_vote_gas_cost(), + ActionPlan::PositionWithdraw(_) => position_withdraw_gas_cost(), + + ActionPlan::Delegate(d) => d.gas_cost(), + ActionPlan::Undelegate(u) => u.gas_cost(), + ActionPlan::ValidatorDefinition(vd) => vd.gas_cost(), ActionPlan::IbcAction(i) => i.gas_cost(), ActionPlan::ProposalSubmit(ps) => ps.gas_cost(), ActionPlan::ProposalWithdraw(pw) => pw.gas_cost(), - ActionPlan::DelegatorVote(_) => delegator_vote_gas_cost(), ActionPlan::ValidatorVote(v) => v.gas_cost(), ActionPlan::ProposalDepositClaim(pdc) => pdc.gas_cost(), ActionPlan::PositionOpen(po) => po.gas_cost(), ActionPlan::PositionClose(pc) => pc.gas_cost(), - ActionPlan::PositionWithdraw(_) => position_withdraw_gas_cost(), ActionPlan::CommunityPoolSpend(ds) => ds.gas_cost(), ActionPlan::CommunityPoolOutput(d) => d.gas_cost(), ActionPlan::CommunityPoolDeposit(dd) => dd.gas_cost(), @@ -289,13 +325,13 @@ impl GasCost for Spend { impl GasCost for Delegate { fn gas_cost(&self) -> Gas { - delegate_gas_cost() + delegate_gas_cost(&self) } } impl GasCost for Undelegate { fn gas_cost(&self) -> Gas { - undelegate_gas_cost() + undelegate_gas_cost(&self) } } @@ -320,9 +356,8 @@ impl GasCost for SwapClaim { impl GasCost for ProposalSubmit { fn gas_cost(&self) -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // The block space measured as the byte length of the encoded action. + block_space: self.encode_to_vec().len() as u64, // In the case of a proposal submission, the compact block cost is zero. // The compact block is only modified it the proposal is ratified. // And when that's the case, the cost is mutualized. @@ -339,9 +374,8 @@ impl GasCost for ProposalSubmit { impl GasCost for ProposalWithdraw { fn gas_cost(&self) -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // The block space measured as the byte length of the encoded action. + block_space: self.encode_to_vec().len() as u64, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. // For a ProposalWithdraw the compact block is not modified. @@ -363,9 +397,8 @@ impl GasCost for DelegatorVote { impl GasCost for ValidatorVote { fn gas_cost(&self) -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // The block space measured as the byte length of the encoded action. + block_space: self.encode_to_vec().len() as u64, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. // For a ValidatorVote the compact block is not modified. @@ -381,9 +414,8 @@ impl GasCost for ValidatorVote { impl GasCost for ProposalDepositClaim { fn gas_cost(&self) -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // The block space measured as the byte length of the encoded action. + block_space: self.encode_to_vec().len() as u64, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. // For a ProposalDepositClaim the compact block is not modified. @@ -399,9 +431,8 @@ impl GasCost for ProposalDepositClaim { impl GasCost for PositionOpen { fn gas_cost(&self) -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // The block space measured as the byte length of the encoded action. + block_space: self.encode_to_vec().len() as u64, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. // For a PositionOpen the compact block is not modified. @@ -417,9 +448,8 @@ impl GasCost for PositionOpen { impl GasCost for PositionClose { fn gas_cost(&self) -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // The block space measured as the byte length of the encoded action. + block_space: self.encode_to_vec().len() as u64, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. // For a PositionClose the compact block is not modified. @@ -441,9 +471,8 @@ impl GasCost for PositionWithdraw { impl GasCost for Ics20Withdrawal { fn gas_cost(&self) -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // The block space measured as the byte length of the encoded action. + block_space: self.encode_to_vec().len() as u64, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. // For a Ics20Withdrawal the compact block is not modified. @@ -459,9 +488,8 @@ impl GasCost for Ics20Withdrawal { impl GasCost for CommunityPoolDeposit { fn gas_cost(&self) -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // The block space measured as the byte length of the encoded action. + block_space: self.encode_to_vec().len() as u64, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. // For a CommunityPoolDeposit the compact block is not modified. @@ -477,9 +505,8 @@ impl GasCost for CommunityPoolDeposit { impl GasCost for CommunityPoolSpend { fn gas_cost(&self) -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // The block space measured as the byte length of the encoded action. + block_space: self.encode_to_vec().len() as u64, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. // For a CommunityPoolSpend the compact block is not modified. @@ -507,9 +534,8 @@ impl GasCost for CommunityPoolOutput { impl GasCost for IbcRelay { fn gas_cost(&self) -> Gas { Gas { - // Each [`Action`] has a `0` `block_space` cost, since the [`Transaction`] itself - // will use the encoded size of the complete transaction to calculate the block space. - block_space: 0, + // The block space measured as the byte length of the encoded action. + block_space: self.encode_to_vec().len() as u64, // The compact block space cost is based on the byte size of the data the [`Action`] adds // to the compact block. // For a IbcAction this is the byte size of a [`StatePayload`]. @@ -531,6 +557,6 @@ impl GasCost for IbcRelay { impl GasCost for ValidatorDefinition { fn gas_cost(&self) -> Gas { - validator_definition_gas_cost() + validator_definition_gas_cost(&self) } } diff --git a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs index 3bcf1abb61..b8272ed531 100644 --- a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs @@ -420,7 +420,7 @@ pub struct OutputBody { #[prost(bytes = "vec", tag = "3")] pub wrapped_memo_key: ::prost::alloc::vec::Vec, /// The key material used for note encryption, wrapped in encryption to the - /// sender's outgoing viewing key. 80 bytes. + /// sender's outgoing viewing key. 48 bytes. #[prost(bytes = "vec", tag = "4")] pub ovk_wrapped_key: ::prost::alloc::vec::Vec, } diff --git a/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto b/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto index 46555ca999..951205572e 100644 --- a/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto +++ b/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto @@ -156,7 +156,7 @@ message OutputBody { // An encrypted key for decrypting the memo. bytes wrapped_memo_key = 3; // The key material used for note encryption, wrapped in encryption to the - // sender's outgoing viewing key. 80 bytes. + // sender's outgoing viewing key. 48 bytes. bytes ovk_wrapped_key = 4; } From 223376faedf43597c57e990b99933258c9710c49 Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Tue, 2 Apr 2024 14:36:20 +0200 Subject: [PATCH 062/168] Add WalletId domain impl (#4146) Domain types implement the trait `DomainType` which adds proto encoding/decoding to the struct. This PR adds it for `WalletId`. This is helpful in the wasm crate when we pass over the wasm-javascript boundary: https://github.com/penumbra-zone/web/pull/846#discussion_r1543488080 --- crates/core/keys/src/keys/wallet_id.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/core/keys/src/keys/wallet_id.rs b/crates/core/keys/src/keys/wallet_id.rs index 263ca93912..f67e03b263 100644 --- a/crates/core/keys/src/keys/wallet_id.rs +++ b/crates/core/keys/src/keys/wallet_id.rs @@ -1,13 +1,17 @@ use serde::{Deserialize, Serialize}; use penumbra_proto::core::keys::v1; -use penumbra_proto::{penumbra::core::keys::v1 as pb, serializers::bech32str}; +use penumbra_proto::{penumbra::core::keys::v1 as pb, serializers::bech32str, DomainType}; /// The hash of a full viewing key, used as an account identifier. #[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[serde(try_from = "pb::WalletId", into = "pb::WalletId")] pub struct WalletId(pub [u8; 32]); +impl DomainType for WalletId { + type Proto = pb::WalletId; +} + impl TryFrom for WalletId { type Error = anyhow::Error; From de0f2de70b85df9988bcf862c70fd406ff66eb67 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Tue, 2 Apr 2024 09:16:05 -0400 Subject: [PATCH 063/168] =?UTF-8?q?docs(summonerd):=20=F0=9F=AA=84=20finis?= =?UTF-8?q?h=20incomplete=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/summonerd/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/summonerd/src/config.rs b/tools/summonerd/src/config.rs index c43ac41455..d316adfb04 100644 --- a/tools/summonerd/src/config.rs +++ b/tools/summonerd/src/config.rs @@ -1,4 +1,4 @@ -/// Configuration for the +/// Configuration for the summoner. #[derive(Clone, Copy)] pub struct Config { pub phase1_timeout_secs: u64, From d7ef8f04966754822743df25ad5061ce5ba3eca3 Mon Sep 17 00:00:00 2001 From: plaidfinch Date: Tue, 2 Apr 2024 13:59:23 -0400 Subject: [PATCH 064/168] Enhance Nix packaging to build the crates and CometBFT (#4145) ## Describe your changes This PR enhances the Nix flake with additional derivations to build Penumbra's binaries and also CometBFT at the correctly compatible version. Do `nix build` to build everything. Targets are: `.#cometbft`, `.#pd`, `.#pcli`, and `.#pclientd`. Do `nix develop` to jump into a shell with everything set up (requires flake support to be turned on in your nix install). ## Checklist before requesting a review - [X] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > This is just adding a Nix derivation for building Penumbra. --------- Co-authored-by: finch --- .envrc.example | 1 + .gitignore | 7 + .../component/dex/src/lp/trading_function.rs | 7 +- flake.lock | 47 ++++++- flake.nix | 122 +++++++++++++++--- rust-toolchain.toml | 2 + 6 files changed, 161 insertions(+), 25 deletions(-) create mode 100644 .envrc.example create mode 100644 rust-toolchain.toml diff --git a/.envrc.example b/.envrc.example new file mode 100644 index 0000000000..3550a30f2d --- /dev/null +++ b/.envrc.example @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index 79e5b6797d..b827c025af 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,13 @@ book # Used for deployments firebase-tmp/ +# Nix build output goes here +result + +# Direnv files +.envrc +.direnv + ## Rust / Cargo # will have compiled files and executables debug/ diff --git a/crates/core/component/dex/src/lp/trading_function.rs b/crates/core/component/dex/src/lp/trading_function.rs index a47e7543b3..b3cd996beb 100644 --- a/crates/core/component/dex/src/lp/trading_function.rs +++ b/crates/core/component/dex/src/lp/trading_function.rs @@ -437,9 +437,10 @@ impl BareTradingFunction { /// Since the fee `f` is bound by `0 <= < 5_000`, we have `1/2 <= gamma <= 1`. /// /// ## Examples: - /// * A fee of 0% (0 bps) results in a discount factor of 1. - /// * A fee of 30 bps (30 bps) results in a discount factor of 0.997. - /// * A fee of 100% (10_000bps) results in a discount factor of 0. + /// + /// * A fee of 0% (0 bps) results in a discount factor of 1. + /// * A fee of 30 bps (30 bps) results in a discount factor of 0.997. + /// * A fee of 100% (10_000bps) results in a discount factor of 0. pub fn gamma(&self) -> U128x128 { (U128x128::from(10_000 - self.fee) / U128x128::from(10_000u64)).expect("10_000 != 0") } diff --git a/flake.lock b/flake.lock index 587cbdef51..b552a9252b 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,25 @@ { "nodes": { + "crane": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1711681752, + "narHash": "sha256-LEg6/dmEFxx6Ygti5DO9MOhGNpyB7zdxdWtzv/FCTXk=", + "owner": "ipetkov", + "repo": "crane", + "rev": "ada0fb4dcce4561acb1eb17c59b7306d9d4a95f3", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -36,8 +56,33 @@ }, "root": { "inputs": { + "crane": "crane", "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1712024007, + "narHash": "sha256-52cf+mHZJbSaDFdsBj6vN1hH52AXsMgEpS/ajzc9yQE=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "d45d957dc3c48792af7ce58eec5d84407655e8fa", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" } }, "systems": { diff --git a/flake.nix b/flake.nix index e2139bf19b..101682614e 100644 --- a/flake.nix +++ b/flake.nix @@ -1,37 +1,117 @@ { - description = "a nix development shell for penumbra"; + description = "A nix development shell and build environment for penumbra"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs = { + nixpkgs.follows = "nixpkgs"; + flake-utils.follows = "flake-utils"; + }; + }; + crane = { + url = "github:ipetkov/crane"; + inputs = { nixpkgs.follows = "nixpkgs"; }; + }; }; - outputs = { self, nixpkgs, flake-utils }: + outputs = { self, nixpkgs, flake-utils, rust-overlay, crane, ... }: flake-utils.lib.eachDefaultSystem (system: let - pkgs = import nixpkgs { - inherit system; + # Define versions of Penumbra and CometBFT + penumbraRelease = null; # Use the local working copy + # penumbraRelease = { # Use a specific release + # version = "0.71.0"; + # sha256 = "sha256-2mpyBEt44UlXm6hahJG9sHGxj6nzh7z9lnj/vLtAAzs="; + # }; + cometBftRelease = { + version = "0.37.5"; + sha256 = "sha256-wNVHsifieAtZgedavCEJLgG0kRDqUhG4Lk5ciTPoNzI="; + vendorHash = "sha256-JPEGMa0HDesEtKFvgLUP2UfTB0DlParepE2p+n06Igc="; }; - nativeBuildInputs = with pkgs; [ - pkg-config - ]; - buildInputs = with pkgs; [ - clang - openssl - rustup - ]; - in - with pkgs; - { - devShells.default = mkShell { - inherit buildInputs nativeBuildInputs; + + # Set up for Rust builds, pinned to the Rust toolchain version in the Penumbra repository + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { inherit system overlays; }; + rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; + craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain; + + # Important environment variables so that the build can find the necessary libraries + PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig"; + LIBCLANG_PATH="${pkgs.libclang.lib}/lib"; + in with pkgs; with pkgs.lib; let + # All the Penumbra binaries + penumbra = (craneLib.buildPackage { + pname = "penumbra"; + src = cleanSourceWith { + src = if penumbraRelease == null then craneLib.path ./. else fetchFromGitHub { + owner = "penumbra-zone"; + repo = "penumbra"; + rev = "v${penumbraRelease.version}"; + sha256 = "${penumbraRelease.sha256}"; + }; + filter = path: type: + # Retain proving and verification parameters, and no-lfs marker file ... + (builtins.match ".*\.(no_lfs|param||bin)$" path != null) || + # ... as well as all the normal cargo source files: + (craneLib.filterCargoSources path type); + }; + nativeBuildInputs = [ pkg-config ]; + buildInputs = [ clang openssl ]; + inherit system PKG_CONFIG_PATH LIBCLANG_PATH; + cargoExtraArgs = "-p pd -p pcli -p pclientd"; + meta = { + description = "A fully private proof-of-stake network and decentralized exchange for the Cosmos ecosystem"; + homepage = "https://penumbra.zone"; + license = [ licenses.mit licenses.asl20 ]; + }; + }).overrideAttrs (_: { doCheck = false; }); # Disable tests to improve build times + + # CometBFT + cometbft = (buildGoModule rec { + pname = "cometbft"; + version = cometBftRelease.version; + subPackages = [ "cmd/cometbft" ]; + src = fetchFromGitHub { + owner = "cometbft"; + repo = "cometbft"; + rev = "v${cometBftRelease.version}"; + hash = cometBftRelease.sha256; + }; + vendorHash = cometBftRelease.vendorHash; + meta = { + description = "CometBFT (fork of Tendermint Core): A distributed, Byzantine fault-tolerant, deterministic state machine replication engine"; + homepage = "https://github.com/cometbft/cometbft"; + license = licenses.asl20; + }; + }).overrideAttrs (_: { doCheck = false; }); # Disable tests to improve build times + in rec { + packages = { inherit penumbra cometbft; }; + apps = { + pd.type = "app"; + pd.program = "${penumbra}/bin/pd"; + pcli.type = "app"; + pcli.program = "${penumbra}/bin/pcli"; + pclientd.type = "app"; + pclientd.program = "${penumbra}/bin/pclientd"; + cometbft.type = "app"; + cometbft.program = "${cometbft}/bin/cometbft"; + }; + defaultPackage = symlinkJoin { + name = "penumbra-and-cometbft"; + paths = [ penumbra cometbft ]; + }; + devShells.default = craneLib.devShell { + inherit LIBCLANG_PATH; + inputsFrom = [ penumbra ]; + packages = [ cargo-watch ]; shellHook = '' - export CC="${pkgs.clang}/bin/clang" - export CXX="${pkgs.clang}/bin/clang++" - export LIBCLANG_PATH="${pkgs.libclang.lib}/lib" + export LIBCLANG_PATH=${LIBCLANG_PATH} + export RUST_SRC_PATH=${pkgs.rustPlatform.rustLibSrc} # Required for rust-analyzer ''; - LIBCLANG_PATH = "${pkgs.llvmPackages.libclang}/lib/libclang.so"; }; } ); diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000000..292fe499e3 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" From e5c1203d6ab73ef83043e05431d826f56516a0f9 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Tue, 2 Apr 2024 18:56:15 -0400 Subject: [PATCH 065/168] staking: enshrine consensus key indexing requirement in unit-test (#4148) --- .../action_handler/validator_definition.rs | 2 +- .../component/stake/src/component/stake.rs | 18 ++- .../stake/src/component/stake/tests.rs | 104 ++++++++++++++++++ .../validator_handler/validator_manager.rs | 8 +- .../validator_handler/validator_store.rs | 32 ++++-- 5 files changed, 142 insertions(+), 22 deletions(-) create mode 100644 crates/core/component/stake/src/component/stake/tests.rs diff --git a/crates/core/component/stake/src/component/action_handler/validator_definition.rs b/crates/core/component/stake/src/component/action_handler/validator_definition.rs index b37e14ad49..192caf76fc 100644 --- a/crates/core/component/stake/src/component/action_handler/validator_definition.rs +++ b/crates/core/component/stake/src/component/action_handler/validator_definition.rs @@ -88,7 +88,7 @@ impl ActionHandler for validator::Definition { // Check if the consensus key is known, and if so, that it is by the // validator that declares it in this definition. if let Some(ck_owner) = state - .get_validator_by_consensus_key(&new_validator.consensus_key) + .get_validator_definition_by_consensus_key(&new_validator.consensus_key) .await? { // If we detect that the new definition tries to squat someone else's diff --git a/crates/core/component/stake/src/component/stake.rs b/crates/core/component/stake/src/component/stake.rs index 6871000caf..5792c6b22e 100644 --- a/crates/core/component/stake/src/component/stake.rs +++ b/crates/core/component/stake/src/component/stake.rs @@ -30,6 +30,9 @@ use crate::component::validator_handler::{ ValidatorDataRead, ValidatorManager, ValidatorUptimeTracker, }; +#[cfg(test)] +mod tests; + pub struct Staking {} impl Staking {} @@ -321,11 +324,16 @@ pub trait StateWriteExt: StateWrite { ) } - async fn register_consensus_key( - &mut self, - identity_key: &IdentityKey, - consensus_key: &PublicKey, - ) { + /// Register a [consensus key][`PublicKey`] in the state, via two verifiable indices: + /// 1. CometBFT address -> [`PublicKey`] + /// 2. [`PublicKey`] -> [`IdentityKey`] + /// + /// # Important note + /// We do not delete obsolete entries on purpose. This is so that + /// the staking component can do evidence attribution even if a byzantine validator + /// has changed the consensus key that was used at the time of the misbehavior. + #[instrument(skip_all)] + fn register_consensus_key(&mut self, identity_key: &IdentityKey, consensus_key: &PublicKey) { let address = self::address::validator_address(consensus_key); tracing::debug!(?identity_key, ?consensus_key, hash = ?hex::encode(address), "registering consensus key"); self.put( diff --git a/crates/core/component/stake/src/component/stake/tests.rs b/crates/core/component/stake/src/component/stake/tests.rs new file mode 100644 index 0000000000..6087318d43 --- /dev/null +++ b/crates/core/component/stake/src/component/stake/tests.rs @@ -0,0 +1,104 @@ +use anyhow::ensure; +use cnidarium::{StateDelta, TempStorage}; +use decaf377_rdsa::SigningKey; +use rand_core::OsRng; +use tendermint::PublicKey; + +use crate::{ + component::{stake::address::validator_address, validator_handler::ValidatorDataRead}, + IdentityKey, StateWriteExt, +}; + +#[tokio::test] +/// Test that we do not delete rotated consensus keys from the [consensus key -> identity key] index.A +/// This is important to maintain because we want to be able to resolve byzantine evidence to a validator's +/// persistent identity even if they have rotated their consensus keys. +async fn test_persistent_identity_by_ck() -> anyhow::Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + let storage = TempStorage::new().await?; + let mut state = StateDelta::new(storage.latest_snapshot()); + + let rng = OsRng; + let persistent_identity = IdentityKey(SigningKey::new(rng).into()); + + let old_ck_raw = ed25519_consensus::SigningKey::new(rng) + .verification_key() + .to_bytes(); + let new_ck_raw = ed25519_consensus::SigningKey::new(rng) + .verification_key() + .to_bytes(); + + let old_ck = PublicKey::from_raw_ed25519(&old_ck_raw).expect("valid vk"); + let new_ck = PublicKey::from_raw_ed25519(&new_ck_raw).expect("valid vk"); + anyhow::ensure!( + old_ck.to_hex() != new_ck.to_hex(), + "the keys must encode to different hex strings for the test to be useful" + ); + + let old_address = validator_address(&old_ck); + let new_address = validator_address(&new_ck); + + state.register_consensus_key(&persistent_identity, &old_ck); + + let retrieved_ck = state + .lookup_consensus_key_by_comet_address(&old_address) + .await + .expect("key is registered"); + + ensure!( + retrieved_ck == old_ck, + "the retrieved consensus key must match the initial ck" + ); + + let retrieved_id = state + .lookup_identity_key_by_consensus_key(&retrieved_ck) + .await + .expect("key is found"); + ensure!( + retrieved_id == persistent_identity, + "the retrieved identity must match its persistent identity" + ); + + state.register_consensus_key(&persistent_identity, &new_ck); + // We want to do a basic check that we can reach for the updated values + // but CRUCIALLY, we want to make sure that we can associate an identity to + // the old consenus key. + let retrieved_ck = state + .lookup_consensus_key_by_comet_address(&new_address) + .await + .expect("key is registered"); + ensure!( + retrieved_ck == new_ck, + "we must be able to find the updated ck" + ); + + let retrieved_id = state + .lookup_identity_key_by_consensus_key(&retrieved_ck) + .await + .expect("key is found"); + ensure!( + retrieved_id == persistent_identity, + "the retrieved id must match the persistent identity, even after an update" + ); + + // CRUCIAL PART: can we find the persistent identity from a rotated comet address/consensus key? + let culprit_ck = state + .lookup_consensus_key_by_comet_address(&old_address) + .await + .expect("key must be found!"); + ensure!( + culprit_ck == old_ck, + "the old address must be associated with the old ck" + ); + + let culprit_id = state + .lookup_identity_key_by_consensus_key(&culprit_ck) + .await + .expect("consensus key -> identity index is persistent across validator updates"); + ensure!( + culprit_id == persistent_identity, + "the retrieved identity must match the persistent identity that we setup" + ); + + Ok(()) +} diff --git a/crates/core/component/stake/src/component/validator_handler/validator_manager.rs b/crates/core/component/stake/src/component/validator_handler/validator_manager.rs index 710f2f04f1..ed68ab8538 100644 --- a/crates/core/component/stake/src/component/validator_handler/validator_manager.rs +++ b/crates/core/component/stake/src/component/validator_handler/validator_manager.rs @@ -462,8 +462,7 @@ pub trait ValidatorManager: StateWrite { // Then, we create a mapping from the validator's consensus key to its // identity key, so we can look up the validator by its consensus key, and // vice-versa. - self.register_consensus_key(&validator_identity, &validator.consensus_key) - .await; + self.register_consensus_key(&validator_identity, &validator.consensus_key); // We register the validator's delegation token in the token registry... self.register_denom(&DelegationToken::from(&validator_identity).denom()) .await; @@ -589,8 +588,7 @@ pub trait ValidatorManager: StateWrite { // Update the consensus key lookup, in case the validator rotated their // consensus key. - self.register_consensus_key(&validator.identity_key, &validator.consensus_key) - .await; + self.register_consensus_key(&validator.identity_key, &validator.consensus_key); self.put(state_key::validators::definitions::by_id(id), validator); @@ -642,7 +640,7 @@ pub trait ValidatorManager: StateWrite { /// Returns an error if the validator is not found in the JMT. async fn process_evidence(&mut self, evidence: &Misbehavior) -> Result<()> { let validator = self - .get_validator_by_cometbft_address(&evidence.validator.address) + .get_validator_definition_by_cometbft_address(&evidence.validator.address) .await? .ok_or_else(|| { anyhow::anyhow!( diff --git a/crates/core/component/stake/src/component/validator_handler/validator_store.rs b/crates/core/component/stake/src/component/validator_handler/validator_store.rs index 7af71788b1..89abf01686 100644 --- a/crates/core/component/stake/src/component/validator_handler/validator_store.rs +++ b/crates/core/component/stake/src/component/validator_handler/validator_store.rs @@ -128,28 +128,38 @@ pub trait ValidatorDataRead: StateRead { self.nonverifiable_get(key.as_bytes()) } + async fn lookup_identity_key_by_consensus_key(&self, ck: &PublicKey) -> Option { + self.get(&state_key::validators::lookup_by::consensus_key(ck)) + .await + .expect("no deserialization error") + } + + async fn lookup_consensus_key_by_comet_address(&self, address: &[u8; 20]) -> Option { + self.get(&state_key::validators::lookup_by::cometbft_address(address)) + .await + .expect("no deserialization error") + } + // Tendermint validators are referenced to us by their Tendermint consensus key, // but we reference them by their Penumbra identity key. - async fn get_validator_by_consensus_key(&self, ck: &PublicKey) -> Result> { - if let Some(identity_key) = self - .get(&state_key::validators::lookup_by::consensus_key(ck)) - .await? - { + async fn get_validator_definition_by_consensus_key( + &self, + ck: &PublicKey, + ) -> Result> { + if let Some(identity_key) = self.lookup_identity_key_by_consensus_key(ck).await { self.get_validator_definition(&identity_key).await } else { return Ok(None); } } - async fn get_validator_by_cometbft_address( + async fn get_validator_definition_by_cometbft_address( &self, address: &[u8; 20], ) -> Result> { - if let Some(consensus_key) = self - .get(&state_key::validators::lookup_by::cometbft_address(address)) - .await? - { - self.get_validator_by_consensus_key(&consensus_key).await + if let Some(consensus_key) = self.lookup_consensus_key_by_comet_address(address).await { + self.get_validator_definition_by_consensus_key(&consensus_key) + .await } else { return Ok(None); } From 46489d35751441389ae0b69f5e89a06c3d06dfd8 Mon Sep 17 00:00:00 2001 From: noot <36753753+noot@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:14:49 -0400 Subject: [PATCH 066/168] cnidarium: implement deferred commits via `StagedWriteBatch` (#4122) Refactor `commit` into `prepare_commit` and `commit_batch`. this allows a `StagedWriteBatch` to be prepared via `prepare_commit` and the actual writing the `StagedWriteBatch` to disk to occur in `commit_batch`. this allows us to get the root hash of the changes without requiring the data to be fully committed. closes #4095 --------- Co-authored-by: Erwan Or --- crates/cnidarium/src/lib.rs | 2 + crates/cnidarium/src/snapshot.rs | 25 +- crates/cnidarium/src/storage.rs | 173 ++++++++++--- crates/cnidarium/src/store/multistore.rs | 26 +- crates/cnidarium/src/store/substore.rs | 2 +- crates/cnidarium/src/write_batch.rs | 57 +++++ crates/cnidarium/tests/write_batch.rs | 302 +++++++++++++++++++++++ 7 files changed, 526 insertions(+), 61 deletions(-) create mode 100644 crates/cnidarium/src/write_batch.rs create mode 100644 crates/cnidarium/tests/write_batch.rs diff --git a/crates/cnidarium/src/lib.rs b/crates/cnidarium/src/lib.rs index bb9d415cf8..a967a60b10 100644 --- a/crates/cnidarium/src/lib.rs +++ b/crates/cnidarium/src/lib.rs @@ -70,6 +70,7 @@ mod store; mod tests; mod utils; mod write; +mod write_batch; #[cfg(feature = "metrics")] pub use crate::metrics::register_metrics; @@ -81,6 +82,7 @@ pub use read::StateRead; pub use snapshot::Snapshot; pub use storage::{Storage, TempStorage}; pub use write::StateWrite; +pub use write_batch::StagedWriteBatch; pub mod future; diff --git a/crates/cnidarium/src/snapshot.rs b/crates/cnidarium/src/snapshot.rs index 706215e41c..c59a652a12 100644 --- a/crates/cnidarium/src/snapshot.rs +++ b/crates/cnidarium/src/snapshot.rs @@ -124,14 +124,15 @@ impl Snapshot { } pub fn prefix_version(&self, prefix: &str) -> Result> { - let config = self + let Some(config) = self .0 .multistore_cache .config - .find_substore(prefix.as_bytes()); - if prefix != config.prefix { - anyhow::bail!("requested substore (prefix={prefix}) does not exist") - } + .find_substore(prefix.as_bytes()) + else { + anyhow::bail!("rquested a version for a prefix that does not exist (prefix={prefix})") + }; + Ok(self.substore_version(&config)) } @@ -145,18 +146,14 @@ impl Snapshot { let rocksdb_snapshot = self.0.snapshot.clone(); let db = self.0.db.clone(); - let config = self + let Some(config) = self .0 .multistore_cache .config - .find_substore(prefix.as_bytes()); - - // If a substore is not found, `find_substore` will default to the main store. - // However, we do not want to mislead the caller by returning a root hash - // that does not correspond to the queried prefix, so we error out instead. - if prefix != config.prefix { - anyhow::bail!("requested substore (prefix={prefix}) does not exist") - } + .find_substore(prefix.as_bytes()) + else { + anyhow::bail!("requested a root for a substore that does not exist (prefix={prefix})") + }; let version = self .substore_version(&config) diff --git a/crates/cnidarium/src/storage.rs b/crates/cnidarium/src/storage.rs index adf8708bb1..350ae8f86d 100644 --- a/crates/cnidarium/src/storage.rs +++ b/crates/cnidarium/src/storage.rs @@ -1,9 +1,9 @@ use std::{path::PathBuf, sync::Arc}; -// use tokio_stream::wrappers::WatchStream; use anyhow::{bail, ensure, Result}; use parking_lot::RwLock; use rocksdb::{Options, DB}; +use std::collections::HashMap; use tokio::sync::watch; use tracing::Span; @@ -15,7 +15,7 @@ use crate::{ substore::{SubstoreConfig, SubstoreSnapshot, SubstoreStorage}, }, }; -use crate::{snapshot_cache::SnapshotCache, StateDelta}; +use crate::{snapshot_cache::SnapshotCache, StagedWriteBatch, StateDelta}; mod temp; pub use temp::TempStorage; @@ -271,9 +271,9 @@ impl Storage { self.0.snapshots.read().get(version) } - /// Commits the provided [`StateDelta`] to persistent storage as the latest - /// version of the chain state. - pub async fn commit(&self, delta: StateDelta) -> Result { + /// Prepares a commit for the provided [`StateDelta`], returning a [`StagedWriteBatch`]. + /// The batch can be committed to the database using the [`Storage::commit_batch`] method. + pub async fn prepare_commit(&self, delta: StateDelta) -> Result { // Extract the snapshot and the changes from the state delta let (snapshot, changes) = delta.flatten(); let prev_snapshot_version = snapshot.version(); @@ -286,35 +286,28 @@ impl Storage { ensure!( prev_storage_version == prev_snapshot_version, - "trying to commit a delta forked from version {}, but the latest version is {}", + "trying to prepare a commit for a delta forked from version {}, but the latest version is {}", prev_snapshot_version, prev_storage_version ); - self.commit_inner(snapshot, changes, next_storage_version, false) + self.prepare_commit_inner(snapshot, changes, next_storage_version, false) .await } - /// Commits the supplied [`Cache`] to persistent storage. - /// - /// # Migrations - /// In the case of chain state migrations we need to commit the new state - /// without incrementing the version. If `perform_migration` is `true` the - /// snapshot will _not_ be written to the snapshot cache, and no subscribers - /// will be notified. Substore versions will not be updated. - async fn commit_inner( + async fn prepare_commit_inner( &self, snapshot: Snapshot, cache: Cache, version: jmt::Version, perform_migration: bool, - ) -> Result { - tracing::debug!(new_jmt_version = ?version, "committing state delta"); + ) -> Result { + tracing::debug!(new_jmt_version = ?version, "preparing to commit state delta"); // Save a copy of the changes to send to subscribers later. let changes = Arc::new(cache.clone_changes()); let mut changes_by_substore = cache.shard_by_prefix(&self.0.multistore_config); - let mut substore_roots = Vec::new(); + let mut substore_roots = HashMap::new(); let mut multistore_versions = multistore::MultistoreCache::from_config(self.0.multistore_config.clone()); @@ -366,16 +359,16 @@ impl Storage { continue; }; - let version = if perform_migration { + let new_version = if perform_migration { old_substore_version } else { old_substore_version.wrapping_add(1) }; - new_versions.push(version); + new_versions.push(new_version); let substore_snapshot = SubstoreSnapshot { config: config.clone(), rocksdb_snapshot: rocksdb_snapshot.clone(), - version, + version: new_version, db: db.clone(), }; @@ -383,7 +376,7 @@ impl Storage { // Commit the substore and collect its root hash let (root_hash, substore_batch) = substore_storage - .commit(changeset, write_batch, version, perform_migration) + .commit(changeset, write_batch, new_version, perform_migration) .await?; write_batch = substore_batch; @@ -393,7 +386,15 @@ impl Storage { ?version, "added substore to write batch" ); - substore_roots.push((config.clone(), root_hash, version)); + substore_roots.insert(config.clone(), (root_hash, new_version)); + + tracing::debug!( + ?root_hash, + prefix = ?config.prefix, + ?new_version, + "updating substore version" + ); + multistore_versions.set_version(config.clone(), new_version); } // Add substore roots to the main store changeset @@ -405,7 +406,7 @@ impl Storage { Cache::default() }); - for (config, root_hash, _) in substore_roots.iter() { + for (config, (root_hash, _)) in substore_roots.iter() { main_store_changes .unwritten_changes .insert(config.prefix.to_string(), Some(root_hash.0.to_vec())); @@ -432,6 +433,110 @@ impl Storage { "added main store to write batch" ); + tracing::debug!(?global_root_hash, version = ?version, "updating main store version"); + let main_store_config = self.0.multistore_config.main_store.clone(); + multistore_versions.set_version(main_store_config, version); + + Ok(StagedWriteBatch { + write_batch, + version, + multistore_versions, + root_hash: global_root_hash, + substore_roots, + perform_migration, + changes, + }) + } + + /// Commits the provided [`StateDelta`] to persistent storage as the latest + /// version of the chain state. + pub async fn commit(&self, delta: StateDelta) -> Result { + let batch = self.prepare_commit(delta).await?; + self.commit_batch(batch) + } + + /// Commits the supplied [`StagedWriteBatch`] to persistent storage. + /// + /// # Migrations + /// In the case of chain state migrations we need to commit the new state + /// without incrementing the version. If `perform_migration` is `true` the + /// snapshot will _not_ be written to the snapshot cache, and no subscribers + /// will be notified. Substore versions will not be updated. + pub fn commit_batch(&self, batch: StagedWriteBatch) -> Result { + let StagedWriteBatch { + write_batch, + version, + multistore_versions, + root_hash: global_root_hash, + substore_roots, + perform_migration, + changes, + } = batch; + + let db = self.0.db.clone(); + + // check that the version of the batch being committed is the correct next version + let old_version = self.latest_version(); + let expected_new_version = if perform_migration { + old_version + } else { + old_version.wrapping_add(1) + }; + + ensure!( + expected_new_version == version, + "new version mismatch: expected {} but got {}", + expected_new_version, + version + ); + + // also check that each of the substore versions are the correct next version + let snapshot = self.latest_snapshot(); + + // Warning: we MUST check version coherence for **every** substore. + // These checks are a second line of defense. They must consider + // the case when two deltas effect distinct substores. + // + // version: (m, ss_1, ss_2) + // D_0: (_, 1, 0) <- initial state + // D_1: (A, 1, 1) <- multiwrite to ss_1 AND ss_2 + // D_1*: (A, 1, 0) <- isolate write to ss_1 + // + // A comprehensive check lets us catch the stale write D_1* even if + // locally it does not directly effect the second substore at all. + // And even if the main version check passes (spuriously, or because of + // a migration). + for (substore_config, new_version) in &multistore_versions.substores { + if substore_config.prefix.is_empty() { + // this is the main store, ignore + continue; + } + + let old_substore_version = snapshot + .substore_version(&substore_config) + .expect("substores must be initialized at startup"); + + // if the substore exists in `substore_roots`, there have been updates to the substore. + // if `perform_migration` is false and there are updates, the next version should be previous + 1. + // otherwise, the version should remain the same. + let expected_substore_version = + if substore_roots.get(substore_config).is_some() && !perform_migration { + old_substore_version.wrapping_add(1) + } else { + old_substore_version + }; + + ensure!( + expected_substore_version == *new_version, + "substore new version mismatch for substore with prefix {}: expected {} but got {}", + substore_config.prefix, + expected_substore_version, + new_version + ); + } + + tracing::debug!(new_jmt_version = ?batch.version, "committing batch to db"); + db.write(write_batch).expect("can write to db"); tracing::debug!( ?global_root_hash, @@ -439,20 +544,6 @@ impl Storage { "committed main store and substores to db" ); - // Update the tracked versions for each substore. - for (config, root_hash, new_version) in substore_roots { - tracing::debug!( - ?root_hash, - prefix = ?config.prefix, - ?new_version, - "updating substore version" - ); - multistore_versions.set_version(config, new_version); - } - - tracing::debug!(?global_root_hash, ?version, "updating main store version"); - multistore_versions.set_version(main_store_config, version); - // If we're not performing a migration, we should update the snapshot cache if !perform_migration { tracing::debug!("updating snapshot cache"); @@ -487,8 +578,10 @@ impl Storage { pub async fn commit_in_place(&self, delta: StateDelta) -> Result { let (snapshot, changes) = delta.flatten(); let old_version = self.latest_version(); - self.commit_inner(snapshot, changes, old_version, true) - .await + let batch = self + .prepare_commit_inner(snapshot, changes, old_version, true) + .await?; + self.commit_batch(batch) } /// Returns the internal handle to RocksDB, this is useful to test adjacent storage crates. diff --git a/crates/cnidarium/src/store/multistore.rs b/crates/cnidarium/src/store/multistore.rs index 3fcb7d8604..c08f6a59e7 100644 --- a/crates/cnidarium/src/store/multistore.rs +++ b/crates/cnidarium/src/store/multistore.rs @@ -15,13 +15,16 @@ impl MultistoreConfig { } /// Returns the substore matching the key's prefix, return `None` otherwise. - pub fn find_substore(&self, key: &[u8]) -> Arc { + pub fn find_substore(&self, key: &[u8]) -> Option> { + if key.is_empty() { + return Some(self.main_store.clone()); + } + // Note: This is a linear search, but the number of substores is small. self.substores .iter() .find(|s| key.starts_with(s.prefix.as_bytes())) .cloned() - .unwrap_or(self.main_store.clone()) } /// Route a key to a substore, and return the truncated key and the corresponding `SubstoreConfig`. @@ -46,7 +49,12 @@ impl MultistoreConfig { /// `prefix_a/` -> `prefix_a/` in `main_store /// `nonexistent_prefix` -> `nonexistent_prefix` in `main_store` pub fn route_key_str<'a>(&self, key: &'a str) -> (&'a str, Arc) { - let config = self.find_substore(key.as_bytes()); + let config = self + .find_substore(key.as_bytes()) + .unwrap_or_else(|| self.main_store.clone()); + + // If the key is a total match, we want to return the key bound to the + // main store. This is where the root hash of the prefix tree is located. if key == config.prefix { return (key, self.main_store.clone()); } @@ -93,7 +101,9 @@ impl MultistoreConfig { /// `prefix_a/` -> `prefix_a/` in `main_store` /// `nonexistent_prefix` -> `nonexistent_prefix` in `main_store` pub fn route_key_bytes<'a>(&self, key: &'a [u8]) -> (&'a [u8], Arc) { - let config = self.find_substore(key); + let config = self + .find_substore(key) + .unwrap_or_else(|| self.main_store.clone()); // If the key is a total match for the prefix, we return the original key // routed to the main store. This is where subtree root hashes are stored. @@ -136,7 +146,9 @@ impl MultistoreConfig { /// `prefix_a/` -> "" in `substore_a` /// `nonexistent_prefix` -> "" in `main_store` pub fn match_prefix_str<'a>(&self, prefix: &'a str) -> (&'a str, Arc) { - let config = self.find_substore(prefix.as_bytes()); + let config = self + .find_substore(prefix.as_bytes()) + .unwrap_or_else(|| self.main_store.clone()); let truncated_prefix = prefix .strip_prefix(&config.prefix) @@ -162,7 +174,9 @@ impl MultistoreConfig { /// `prefix_a/` -> "" in `substore_a` /// `nonexistent_prefix` -> "" in `main_store` pub fn match_prefix_bytes<'a>(&self, prefix: &'a [u8]) -> (&'a [u8], Arc) { - let config = self.find_substore(prefix); + let config = self + .find_substore(prefix) + .unwrap_or_else(|| self.main_store.clone()); let truncated_prefix = prefix .strip_prefix(config.prefix.as_bytes()) diff --git a/crates/cnidarium/src/store/substore.rs b/crates/cnidarium/src/store/substore.rs index ad9eadfdf9..9fc6b68521 100644 --- a/crates/cnidarium/src/store/substore.rs +++ b/crates/cnidarium/src/store/substore.rs @@ -18,7 +18,7 @@ use jmt::storage::TreeWriter; /// Specifies the configuration of a substore, which is a prefixed subset of /// the main store with its own merkle tree, nonverifiable data, preimage index, etc. -#[derive(Debug, Eq, PartialEq, PartialOrd, Ord)] +#[derive(Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct SubstoreConfig { /// The prefix of the substore. If empty, it is the root-level store config. pub prefix: String, diff --git a/crates/cnidarium/src/write_batch.rs b/crates/cnidarium/src/write_batch.rs new file mode 100644 index 0000000000..a70f63cb24 --- /dev/null +++ b/crates/cnidarium/src/write_batch.rs @@ -0,0 +1,57 @@ +use std::sync::Arc; + +use std::collections::HashMap; + +use crate::{ + cache::Cache, + store::{multistore, substore::SubstoreConfig}, + RootHash, +}; + +/// A staged write batch that can be committed to RocksDB. +/// +/// This allows for write batches to be prepared and committed at a later time. +pub struct StagedWriteBatch { + /// The write batch to commit to RocksDB. + pub(crate) write_batch: rocksdb::WriteBatch, + /// The new version of the chain state. + pub(crate) version: jmt::Version, + /// The new versions of each substore. + pub(crate) multistore_versions: multistore::MultistoreCache, + /// The root hash of the chain state corresponding to this set of changes. + pub(crate) root_hash: RootHash, + /// The configs, root hashes, and new versions of each substore + /// that was updated in this batch. + pub(crate) substore_roots: HashMap, (RootHash, u64)>, + /// Whether or not to perform a migration. + pub(crate) perform_migration: bool, + /// A lightweight copy of the changeset, this is useful to provide + /// a stream of changes to subscribers. + pub(crate) changes: Arc, +} + +impl StagedWriteBatch { + /// Returns the new version of the chain state corresponding to this set of changes. + pub fn version(&self) -> jmt::Version { + self.version + } + + /// Returns the root hash of the jmt corresponding to this set of changes. + pub fn root_hash(&self) -> &RootHash { + &self.root_hash + } + + /// Returns the version of a substore in this batch, if it exists + /// and `None` otherwise. + pub fn substore_version(&self, prefix: &str) -> Option { + let Some(substore_config) = self + .multistore_versions + .config + .find_substore(prefix.as_bytes()) + else { + return None; + }; + + self.multistore_versions.get_version(&substore_config) + } +} diff --git a/crates/cnidarium/tests/write_batch.rs b/crates/cnidarium/tests/write_batch.rs new file mode 100644 index 0000000000..0717400fd0 --- /dev/null +++ b/crates/cnidarium/tests/write_batch.rs @@ -0,0 +1,302 @@ +use anyhow::Result; +use cnidarium::{StateDelta, StateWrite, Storage}; +use tempfile; +use tokio; + +#[tokio::test] +/// A simple test that checks that we cannot commit a stale batch to storage. +/// Strategy: +/// Create three state deltas, one that writes to every substore, and two others +/// that target specific substores or none at all. +pub async fn test_write_batch_stale_version_substores() -> Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + let tmpdir = tempfile::tempdir()?; + let db_path = tmpdir.into_path(); + let substore_prefixes = vec![ + "ibc".to_string(), + "dex".to_string(), + "misc".to_string(), + "cometbft-data".to_string(), + ]; + let storage = Storage::load(db_path.clone(), substore_prefixes.clone()).await?; + let initial_snapshot = storage.latest_snapshot(); + let initial_version = initial_snapshot.version(); + let initial_root_hash = initial_snapshot.root_hash().await?; + assert_eq!( + initial_version, + u64::MAX, + "initial version should be u64::MAX" + ); + assert_eq!(initial_root_hash.0, [0u8; 32]); + + /* ************************ Prepare three deltas ************************** */ + // Our goal is to check that we can't commit a batch with a stale version. + // We create three deltas: + // 1. Empty delta + // 2. Delta that writes to one substore + // 3. Delta that writes to each substore and also writes to the main store + + /* We create an empty delta that writes no keys. */ + let delta_1 = StateDelta::new(initial_snapshot); + let write_batch_1 = storage.prepare_commit(delta_1).await?; + let version_1 = write_batch_1.version(); + let root_hash_1 = write_batch_1.root_hash().clone(); + assert_eq!(version_1, initial_version.wrapping_add(1)); + assert_ne!(root_hash_1.0, initial_root_hash.0); + + // We check that merely preparing a batch does not write anything. + let state_snapshot = storage.latest_snapshot(); + assert_eq!(state_snapshot.version(), initial_version); + assert_eq!(state_snapshot.root_hash().await?.0, initial_root_hash.0); + for prefix in substore_prefixes.iter() { + // We didn't write to any substores, so their version should be unchanged. + assert_eq!( + write_batch_1 + .substore_version(prefix) + .expect("substore exists"), + u64::MAX + ) + } + + /* We create a new delta that writes to a single substore. */ + let mut delta_2 = StateDelta::new(state_snapshot.clone()); + delta_2.put_raw("ibc/key".to_string(), [1u8; 32].to_vec()); + let write_batch_2 = storage.prepare_commit(delta_2).await?; + let version_2 = write_batch_2.version(); + let root_hash_2 = write_batch_2.root_hash(); + assert_eq!(version_2, initial_version.wrapping_add(1)); + assert_ne!(root_hash_2.0, initial_root_hash.0); + + // Now, we check that the version for the main store is incremented, and + // only the version for the ibc substore is incremented. + assert_eq!(write_batch_2.version(), initial_version.wrapping_add(1)); + assert_eq!( + write_batch_2 + .substore_version("ibc") + .expect("substore_exists"), + initial_version.wrapping_add(1) + ); + for prefix in substore_prefixes.iter().filter(|p| *p != "ibc") { + assert_eq!( + write_batch_2 + .substore_version(prefix) + .expect("substore exists"), + u64::MAX + ) + } + + /* We create a new delta that writes to each substore. */ + let mut delta_3 = StateDelta::new(state_snapshot); + for substore_prefix in substore_prefixes.iter() { + let key = format!("{}/key", substore_prefix); + tracing::debug!(?key, "adding to delta_1"); + delta_3.put_raw(key, [1u8; 32].to_vec()); + } + let write_batch_3 = storage.prepare_commit(delta_3).await?; + let version_3 = write_batch_3.version(); + let root_hash_3 = write_batch_3.root_hash().clone(); + + // Once again, we check that we incremented the main store version. + assert_eq!(version_3, initial_version.wrapping_add(1)); + assert_ne!(root_hash_3.0, initial_root_hash.0); + // In addition to that, we check that we incremented the version of each substore. + for prefix in substore_prefixes.iter() { + assert_eq!( + write_batch_3 + .substore_version(prefix) + .expect("substore exists"), + initial_version.wrapping_add(1) + ) + } + + /* Persist `write_batch_1` and check that the two other (stale) deltas cannot be applied. */ + let final_root = storage + .commit_batch(write_batch_1) + .expect("committing batch 3 should work"); + let final_snapshot = storage.latest_snapshot(); + assert_eq!(root_hash_1.0, final_root.0); + assert_eq!(root_hash_1.0, final_snapshot.root_hash().await?.0); + assert_eq!(version_1, final_snapshot.version()); + assert!( + storage.commit_batch(write_batch_2).is_err(), + "committing batch 2 should fail" + ); + assert!( + storage.commit_batch(write_batch_3).is_err(), + "committing batch 3 should fail" + ); + + Ok(()) +} + +#[tokio::test] +/// Test that we can commit a batch without incrementing the substore versions if there are no +/// keys to write. +pub async fn test_two_empty_writes() -> Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + let tmpdir = tempfile::tempdir()?; + let db_path = tmpdir.into_path(); + let substore_prefixes = vec![ + "ibc".to_string(), + "dex".to_string(), + "misc".to_string(), + "cometbft-data".to_string(), + ]; + let storage = Storage::load(db_path.clone(), substore_prefixes.clone()).await?; + let initial_snapshot = storage.latest_snapshot(); + let initial_version = initial_snapshot.version(); + let initial_root_hash = initial_snapshot.root_hash().await?; + assert_eq!( + initial_version, + u64::MAX, + "initial version should be u64::MAX" + ); + assert_eq!(initial_root_hash.0, [0u8; 32]); + + let mut delta_1 = StateDelta::new(initial_snapshot); + for substore_prefix in substore_prefixes.iter() { + let key = format!("{}/key", substore_prefix); + tracing::debug!(?key, "adding to delta_1"); + delta_1.put_raw(key, [1u8; 12].to_vec()); + } + let write_batch_1 = storage.prepare_commit(delta_1).await?; + let version_1 = write_batch_1.version(); + let root_hash_1 = write_batch_1.root_hash().clone(); + + assert_eq!(version_1, initial_version.wrapping_add(1)); + assert_ne!(root_hash_1.0, initial_root_hash.0); + for prefix in substore_prefixes.iter() { + assert_eq!( + write_batch_1 + .substore_version(prefix) + .expect("substore exists"), + initial_version.wrapping_add(1) + ) + } + + // We check that merely preparing a batch does not write anything. + let state_snapshot = storage.latest_snapshot(); + assert_eq!(state_snapshot.version(), initial_version); + assert_eq!(state_snapshot.root_hash().await?.0, initial_root_hash.0); + + /* We create a new delta that writes no keys */ + let delta_2 = StateDelta::new(state_snapshot.clone()); + let write_batch_2 = storage.prepare_commit(delta_2).await?; + let version_2 = write_batch_2.version(); + let root_hash_2 = write_batch_2.root_hash(); + assert_eq!(version_2, initial_version.wrapping_add(1)); + assert_ne!(root_hash_2.0, initial_root_hash.0); + assert_eq!(write_batch_2.version(), initial_version.wrapping_add(1)); + for prefix in substore_prefixes.iter() { + assert_eq!( + write_batch_2 + .substore_version(prefix) + .expect("substore exists"), + initial_version + ) + } + + let block_1_root = storage + .commit_batch(write_batch_1) + .expect("committing batch 3 should work"); + let block_1_snapshot = storage.latest_snapshot(); + let block_1_version = block_1_snapshot.version(); + assert_eq!(root_hash_1.0, block_1_root.0); + assert_eq!(root_hash_1.0, block_1_snapshot.root_hash().await?.0); + assert_eq!(version_1, block_1_version); + assert!( + storage.commit_batch(write_batch_2).is_err(), + "committing batch 2 should fail" + ); + + /* We create an empty delta that writes no keys. */ + let delta_3 = StateDelta::new(block_1_snapshot); + let write_batch_3 = storage.prepare_commit(delta_3).await?; + let version_3 = write_batch_3.version(); + let root_hash_3 = write_batch_3.root_hash().clone(); + assert_eq!(version_3, block_1_version.wrapping_add(1)); + + /* Check that we can apply `write_batch_3` */ + let block_2_root = storage + .commit_batch(write_batch_3) + .expect("committing batch 3 should work"); + let block_2_snapshot = storage.latest_snapshot(); + let block_2_version = block_2_snapshot.version(); + assert_eq!(root_hash_3.0, block_2_root.0); + assert_eq!(root_hash_3.0, block_2_snapshot.root_hash().await?.0); + assert_eq!(version_3, block_2_version); + Ok(()) +} + +#[tokio::test] +/// Test that we can write prepare-commit batches that write to every +/// substore. +/// Intuition: we want to make sure that the version check that guards us from +/// writing stale batches, is working as expected. +pub async fn test_batch_substore() -> Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + let tmpdir = tempfile::tempdir()?; + let db_path = tmpdir.into_path(); + let substore_prefixes = vec![ + "ibc".to_string(), + "dex".to_string(), + "misc".to_string(), + "cometbft-data".to_string(), + ]; + let storage = Storage::load(db_path.clone(), substore_prefixes.clone()).await?; + let initial_snapshot = storage.latest_snapshot(); + let initial_version = initial_snapshot.version(); + let initial_root_hash = initial_snapshot.root_hash().await?; + assert_eq!( + initial_version, + u64::MAX, + "initial version should be u64::MAX" + ); + assert_eq!(initial_root_hash.0, [0u8; 32]); + + for i in 0..100 { + let snapshot = storage.latest_snapshot(); + let prev_version = snapshot.version(); + let prev_root = snapshot + .root_hash() + .await + .expect("a root hash is available"); + + let mut delta = StateDelta::new(snapshot); + for substore_prefix in substore_prefixes.iter() { + let key = format!("{}/key_{i}", substore_prefix); + tracing::debug!(?key, index = i, "adding to delta"); + delta.put_raw(key, [1u8; 12].to_vec()); + } + let write_batch = storage.prepare_commit(delta).await?; + let next_version = write_batch.version(); + let next_root = write_batch.root_hash().clone(); + + assert_eq!(next_version, prev_version.wrapping_add(1)); + assert_ne!(next_root.0, prev_root.0); + for prefix in substore_prefixes.iter() { + assert_eq!( + write_batch + .substore_version(prefix) + .expect("substore exists"), + prev_version.wrapping_add(1) + ) + } + + // We check that merely preparing a batch does not write anything. + let state_snapshot = storage.latest_snapshot(); + assert_eq!(state_snapshot.version(), prev_version); + assert_eq!(state_snapshot.root_hash().await?.0, prev_root.0); + + let block_root = storage + .commit_batch(write_batch) + .expect("committing batch 3 should work"); + let block_snapshot = storage.latest_snapshot(); + let block_version = block_snapshot.version(); + assert_eq!(next_root.0, block_root.0); + assert_eq!(next_root.0, block_snapshot.root_hash().await?.0); + assert_eq!(next_version, block_version); + } + + Ok(()) +} From f9e6da6514d12a9ad9c5008a96115e4ba1803895 Mon Sep 17 00:00:00 2001 From: Conor Schaefer Date: Wed, 3 Apr 2024 16:04:10 -0700 Subject: [PATCH 067/168] ci: use nightly rust for mdbook installation Docs builds broke after merge of [0], not because its settings were incompatible, but because its inclusion of a `rust-toolchain.toml` file busted the CI cache, and caused the `mdbook` dep to be freshly installed, rather than retrieved from cache. We don't version-lock mdbook and friends, and the latest version evidently bumped MSRV to 1.74, which is newer than what we set for MSRV. We should probably bump MSRV too, but I'll do that separately. This commit is to unbreak the docs builds. [0] https://github.com/penumbra-zone/penumbra/pull/4145 --- .github/workflows/notes.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/notes.yml b/.github/workflows/notes.yml index 26215dbf02..26c4faf81e 100644 --- a/.github/workflows/notes.yml +++ b/.github/workflows/notes.yml @@ -30,7 +30,8 @@ jobs: # Ostensibly building from source, but the cache-loading above # ensures we don't need to rebuild frequently. - name: Install mdbook dependencies - run: cargo install mdbook mdbook-katex mdbook-mermaid mdbook-linkcheck + # Make sure to install with `+nightly`, because that's the toolchain we'll use for building docs. + run: cargo +nightly install mdbook mdbook-katex mdbook-mermaid mdbook-linkcheck - name: Build software guide run: cd docs/guide && mdbook build From de135e807d4e883fb4fb5e0757748e38cd2d8719 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Wed, 3 Apr 2024 20:25:58 -0400 Subject: [PATCH 068/168] stake: `VerificationKeyBytes` in `IdentityKey` (#4152) ## issue ticket number and link fixes #2304. ## checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > only changes internal representation --------- Co-authored-by: Henry de Valence --- crates/bin/pcli/src/command/validator.rs | 12 ++++++------ crates/bin/pcli/tests/proof.rs | 2 +- crates/bin/pd/src/testnet/generate.rs | 4 ++-- .../app_can_define_and_delegate_to_a_validator.rs | 4 ++-- ..._tracks_uptime_for_validators_only_once_active.rs | 4 ++-- .../core/app/tests/common/test_node_builder_ext.rs | 2 +- .../component/action_handler/validator_definition.rs | 7 +++---- .../component/stake/src/component/stake/tests.rs | 5 +++-- crates/core/component/stake/src/delegation_token.rs | 5 +++-- crates/core/component/stake/src/identity_key.rs | 8 ++++---- crates/core/component/stake/src/rate.rs | 4 ++-- crates/core/component/stake/src/state_key.rs | 8 ++++---- crates/core/component/stake/src/unbonding_token.rs | 5 +++-- .../component/stake/src/undelegate_claim/proof.rs | 3 ++- 14 files changed, 38 insertions(+), 35 deletions(-) diff --git a/crates/bin/pcli/src/command/validator.rs b/crates/bin/pcli/src/command/validator.rs index d1c0264457..6f9e28108c 100644 --- a/crates/bin/pcli/src/command/validator.rs +++ b/crates/bin/pcli/src/command/validator.rs @@ -158,11 +158,11 @@ impl ValidatorCmd { match self { ValidatorCmd::Identity { base64 } => { - let ik = IdentityKey(fvk.spend_verification_key().clone()); + let ik = IdentityKey(fvk.spend_verification_key().clone().into()); if *base64 { use base64::{display::Base64Display, engine::general_purpose::STANDARD}; - println!("{}", Base64Display::new(&ik.0.to_bytes(), &STANDARD)); + println!("{}", Base64Display::new(ik.0.as_ref(), &STANDARD)); } else { println!("{ik}"); } @@ -276,7 +276,7 @@ impl ValidatorCmd { reason, signature_file, }) => { - let identity_key = IdentityKey(fvk.spend_verification_key().clone()); + let identity_key = IdentityKey(fvk.spend_verification_key().clone().into()); let governance_key = app.config.governance_key(); let (proposal, vote): (u64, Vote) = (*vote).into(); @@ -323,7 +323,7 @@ impl ValidatorCmd { reason, signature, }) => { - let identity_key = IdentityKey(fvk.spend_verification_key().clone()); + let identity_key = IdentityKey(fvk.spend_verification_key().clone().into()); let governance_key = app.config.governance_key(); let (proposal, vote): (u64, Vote) = (*vote).into(); @@ -385,7 +385,7 @@ impl ValidatorCmd { tendermint_validator_keyfile, }) => { let (address, _dtk) = fvk.incoming().payment_address(0u32.into()); - let identity_key = IdentityKey(fvk.spend_verification_key().clone()); + let identity_key = IdentityKey(fvk.spend_verification_key().clone().into()); // By default, the template sets the governance key to the same verification key as // the identity key, but a validator can change this if they want to use different // key material. @@ -473,7 +473,7 @@ impl ValidatorCmd { } } ValidatorCmd::Definition(DefinitionCmd::Fetch { file }) => { - let identity_key = IdentityKey(fvk.spend_verification_key().clone()); + let identity_key = IdentityKey(fvk.spend_verification_key().clone().into()); super::query::ValidatorCmd::Definition { file: file.clone(), identity_key: identity_key.to_string(), diff --git a/crates/bin/pcli/tests/proof.rs b/crates/bin/pcli/tests/proof.rs index a3eacf3860..adaa09277e 100644 --- a/crates/bin/pcli/tests/proof.rs +++ b/crates/bin/pcli/tests/proof.rs @@ -425,7 +425,7 @@ fn undelegate_claim_parameters_vs_current_undelegate_claim_circuit() { let balance_blinding = Fr::from(1u8); let value1_amount = 1u64; let penalty_amount = 1u64; - let validator_identity = IdentityKey((&sk).into()); + let validator_identity = IdentityKey(VerificationKey::from(&sk).into()); let unbonding_amount = Amount::from(value1_amount); let start_height = 1; diff --git a/crates/bin/pd/src/testnet/generate.rs b/crates/bin/pd/src/testnet/generate.rs index f063c02afd..d8585200f1 100644 --- a/crates/bin/pd/src/testnet/generate.rs +++ b/crates/bin/pd/src/testnet/generate.rs @@ -457,7 +457,7 @@ impl TestnetValidator { let ivk = fvk.incoming(); let (dest, _dtk_d) = ivk.payment_address(0u32.into()); - let identity_key: IdentityKey = IdentityKey(fvk.spend_verification_key().clone()); + let identity_key: IdentityKey = IdentityKey(fvk.spend_verification_key().clone().into()); let delegation_denom = DelegationToken::from(&identity_key).denom(); Ok(Allocation { address: dest, @@ -540,7 +540,7 @@ impl TryFrom<&TestnetValidator> for Validator { // Currently there's no way to set validator keys beyond // manually editing the genesis.json. Otherwise they // will be randomly generated keys. - identity_key: IdentityKey(tv.keys.validator_id_vk), + identity_key: IdentityKey(tv.keys.validator_id_vk.into()), governance_key: GovernanceKey(tv.keys.validator_id_vk), consensus_key: tv.keys.validator_cons_pk, name: tv.name.clone(), diff --git a/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs index 25f6cdb4a9..4d59dfab20 100644 --- a/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs +++ b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs @@ -4,7 +4,7 @@ use { self::common::BuilderExt, anyhow::{anyhow, Context}, cnidarium::TempStorage, - decaf377_rdsa::{SigningKey, SpendAuth}, + decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey}, penumbra_app::{genesis::AppState, server::consensus::Consensus}, penumbra_keys::test_keys, penumbra_mock_client::MockClient, @@ -115,7 +115,7 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { // To define a validator, we need to define two keypairs: an identity key // for the Penumbra application and a consensus key for cometbft. let new_validator_id_sk = SigningKey::::new(OsRng); - let new_validator_id = IdentityKey(new_validator_id_sk.into()); + let new_validator_id = IdentityKey(VerificationKey::from(&new_validator_id_sk).into()); let new_validator_consensus_sk = ed25519_consensus::SigningKey::new(OsRng); let new_validator_consensus = new_validator_consensus_sk.verification_key(); diff --git a/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs b/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs index 70b2bdc041..de687e553c 100644 --- a/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs +++ b/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs @@ -3,7 +3,7 @@ mod common; use { self::common::BuilderExt, cnidarium::TempStorage, - decaf377_rdsa::{SigningKey, SpendAuth}, + decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey}, penumbra_app::{genesis::AppState, server::consensus::Consensus}, penumbra_keys::test_keys, penumbra_mock_client::MockClient, @@ -90,7 +90,7 @@ async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<( // To define a validator, we need to define two keypairs: an identity key // for the Penumbra application and a consensus key for cometbft. let new_validator_id_sk = SigningKey::::new(OsRng); - let new_validator_id = IdentityKey(new_validator_id_sk.into()); + let new_validator_id = IdentityKey(VerificationKey::from(&new_validator_id_sk).into()); let new_validator_consensus_sk = ed25519_consensus::SigningKey::new(OsRng); let new_validator_consensus = new_validator_consensus_sk.verification_key(); diff --git a/crates/core/app/tests/common/test_node_builder_ext.rs b/crates/core/app/tests/common/test_node_builder_ext.rs index 6362e6343d..0e586dfdcd 100644 --- a/crates/core/app/tests/common/test_node_builder_ext.rs +++ b/crates/core/app/tests/common/test_node_builder_ext.rs @@ -91,7 +91,7 @@ fn generate_penumbra_validator( .incoming() .payment_address(0u32.into()); - let ik = penumbra_stake::IdentityKey(validator_id_vk); + let ik = penumbra_stake::IdentityKey(validator_id_vk.into()); let delegation_denom = DelegationToken::from(ik).denom(); let allocation = Allocation { diff --git a/crates/core/component/stake/src/component/action_handler/validator_definition.rs b/crates/core/component/stake/src/component/action_handler/validator_definition.rs index 192caf76fc..4eb38eb5f1 100644 --- a/crates/core/component/stake/src/component/action_handler/validator_definition.rs +++ b/crates/core/component/stake/src/component/action_handler/validator_definition.rs @@ -9,6 +9,7 @@ use crate::{ use anyhow::{ensure, Context, Result}; use async_trait::async_trait; use cnidarium::StateWrite; +use decaf377_rdsa::VerificationKey; use penumbra_proto::DomainType; #[async_trait] @@ -36,10 +37,8 @@ impl ActionHandler for validator::Definition { // Then, we check the signature: let definition_bytes = self.validator.encode_to_vec(); - self.validator - .identity_key - .0 - .verify(&definition_bytes, &self.auth_sig) + VerificationKey::try_from(self.validator.identity_key.0) + .and_then(|vk| vk.verify(&definition_bytes, &self.auth_sig)) .context("validator definition signature failed to verify")?; let total_funding_bps = self diff --git a/crates/core/component/stake/src/component/stake/tests.rs b/crates/core/component/stake/src/component/stake/tests.rs index 6087318d43..5aa3a5d519 100644 --- a/crates/core/component/stake/src/component/stake/tests.rs +++ b/crates/core/component/stake/src/component/stake/tests.rs @@ -1,6 +1,6 @@ use anyhow::ensure; use cnidarium::{StateDelta, TempStorage}; -use decaf377_rdsa::SigningKey; +use decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey}; use rand_core::OsRng; use tendermint::PublicKey; @@ -19,7 +19,8 @@ async fn test_persistent_identity_by_ck() -> anyhow::Result<()> { let mut state = StateDelta::new(storage.latest_snapshot()); let rng = OsRng; - let persistent_identity = IdentityKey(SigningKey::new(rng).into()); + let vk = VerificationKey::from(SigningKey::::new(OsRng)); + let persistent_identity = IdentityKey(vk.into()); let old_ck_raw = ed25519_consensus::SigningKey::new(rng) .verification_key() diff --git a/crates/core/component/stake/src/delegation_token.rs b/crates/core/component/stake/src/delegation_token.rs index c0bc4509de..9a92358736 100644 --- a/crates/core/component/stake/src/delegation_token.rs +++ b/crates/core/component/stake/src/delegation_token.rs @@ -123,7 +123,7 @@ impl std::hash::Hash for DelegationToken { #[cfg(test)] mod tests { - use decaf377_rdsa::{SigningKey, SpendAuth}; + use decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey}; use super::*; @@ -131,7 +131,8 @@ mod tests { fn delegation_token_denomination_round_trip() { use rand_core::OsRng; - let ik = IdentityKey(SigningKey::::new(OsRng).into()); + let vk = VerificationKey::from(SigningKey::::new(OsRng)); + let ik = IdentityKey(vk.into()); let token = DelegationToken::new(ik); diff --git a/crates/core/component/stake/src/identity_key.rs b/crates/core/component/stake/src/identity_key.rs index 96d7ece5af..3ffc9e19b9 100644 --- a/crates/core/component/stake/src/identity_key.rs +++ b/crates/core/component/stake/src/identity_key.rs @@ -7,7 +7,7 @@ use penumbra_proto::{ }; use serde::{Deserialize, Serialize}; -use decaf377_rdsa::{SpendAuth, VerificationKey}; +use decaf377_rdsa::{SpendAuth, VerificationKeyBytes}; /// The root of a validator's identity. /// @@ -20,7 +20,7 @@ use decaf377_rdsa::{SpendAuth, VerificationKey}; /// designed for custodying funds to protect their identity. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[serde(try_from = "pb::IdentityKey", into = "pb::IdentityKey")] -pub struct IdentityKey(pub VerificationKey); +pub struct IdentityKey(pub VerificationKeyBytes); // IMPORTANT: Changing this implementation is state-breaking. impl std::str::FromStr for IdentityKey { @@ -37,7 +37,7 @@ impl std::str::FromStr for IdentityKey { impl std::fmt::Display for IdentityKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&bech32str::encode( - &self.0.to_bytes(), + self.0.as_ref(), BECH32_PREFIX, bech32str::Bech32m, )) @@ -57,7 +57,7 @@ impl DomainType for IdentityKey { impl From for pb::IdentityKey { fn from(ik: IdentityKey) -> Self { pb::IdentityKey { - ik: ik.0.to_bytes().to_vec(), + ik: ik.0.as_ref().to_vec(), } } } diff --git a/crates/core/component/stake/src/rate.rs b/crates/core/component/stake/src/rate.rs index bc8b07fa04..435b24c5e0 100644 --- a/crates/core/component/stake/src/rate.rs +++ b/crates/core/component/stake/src/rate.rs @@ -400,8 +400,8 @@ mod tests { #[test] fn slash_rate_by_penalty() { - let sk = rdsa::SigningKey::new(OsRng); - let ik = IdentityKey((&sk).into()); + let vk = rdsa::VerificationKey::from(rdsa::SigningKey::new(OsRng)); + let ik = IdentityKey(vk.into()); let rate_data = RateData { identity_key: ik, diff --git a/crates/core/component/stake/src/state_key.rs b/crates/core/component/stake/src/state_key.rs index 4a75380569..50a87e680c 100644 --- a/crates/core/component/stake/src/state_key.rs +++ b/crates/core/component/stake/src/state_key.rs @@ -168,8 +168,8 @@ mod tests { #[test] fn penalty_in_epoch_padding() { - let sk = rdsa::SigningKey::new(OsRng); - let ik = IdentityKey((&sk).into()); + let vk = rdsa::VerificationKey::from(rdsa::SigningKey::new(OsRng)); + let ik = IdentityKey(vk.into()); assert_eq!( penalty::for_id_in_epoch(&ik, 791), @@ -180,8 +180,8 @@ mod tests { #[test] fn penalty_in_epoch_sorting() { - let sk = rdsa::SigningKey::new(OsRng); - let ik = IdentityKey((&sk).into()); + let vk = rdsa::VerificationKey::from(rdsa::SigningKey::new(OsRng)); + let ik = IdentityKey(vk.into()); let k791 = penalty::for_id_in_epoch(&ik, 791); let k792 = penalty::for_id_in_epoch(&ik, 792); diff --git a/crates/core/component/stake/src/unbonding_token.rs b/crates/core/component/stake/src/unbonding_token.rs index d3344f7f39..c6921d9e33 100644 --- a/crates/core/component/stake/src/unbonding_token.rs +++ b/crates/core/component/stake/src/unbonding_token.rs @@ -136,7 +136,7 @@ impl std::hash::Hash for UnbondingToken { #[cfg(test)] mod tests { - use decaf377_rdsa::{SigningKey, SpendAuth}; + use decaf377_rdsa::{SigningKey, VerificationKey}; use super::*; @@ -144,7 +144,8 @@ mod tests { fn unbonding_token_denomination_round_trip() { use rand_core::OsRng; - let ik = IdentityKey(SigningKey::::new(OsRng).into()); + let vk = VerificationKey::from(SigningKey::new(OsRng)); + let ik = IdentityKey(vk.into()); let start = 782; let token = UnbondingToken::new(ik, start); diff --git a/crates/core/component/stake/src/undelegate_claim/proof.rs b/crates/core/component/stake/src/undelegate_claim/proof.rs index bf1c6e4f8c..607da700ae 100644 --- a/crates/core/component/stake/src/undelegate_claim/proof.rs +++ b/crates/core/component/stake/src/undelegate_claim/proof.rs @@ -106,6 +106,7 @@ mod tests { use penumbra_proof_params::generate_prepared_test_parameters; use proptest::prelude::*; use rand_core::OsRng; + use rdsa::VerificationKey; use crate::{IdentityKey, Penalty, UnbondingToken}; use penumbra_shielded_pool::ConvertCircuit; @@ -124,7 +125,7 @@ mod tests { let (pk, vk) = generate_prepared_test_parameters::(&mut rng); let sk = rdsa::SigningKey::new_from_field(validator_randomness); - let validator_identity = IdentityKey((&sk).into()); + let validator_identity = IdentityKey(VerificationKey::from(&sk).into()); let unbonding_amount = Amount::from(value1_amount); let start_epoch_index = 1; From 25b27aef44e8850ae4d767d208937e085e34fb67 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Thu, 4 Apr 2024 11:23:22 -0400 Subject: [PATCH 069/168] dex: add `max_positions_per_pair` to parameters (#4158) ## Describe your changes This adds a `max_positions_per_pair` parameter to the DEX component, in preparation for #4077. ## Checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > This is backward compatible with `v0.71.0` since the field will be ignored/default to zero if not found in the message --- .../src/gen/proto_descriptor.bin.no_lfs | Bin 88631 -> 95074 bytes crates/core/app/src/params/change.rs | 4 +++- crates/core/component/dex/src/params.rs | 5 +++++ .../src/gen/penumbra.core.component.dex.v1.rs | 5 +++++ .../penumbra.core.component.dex.v1.serde.rs | 20 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 382778 -> 379727 bytes .../penumbra/core/component/dex/v1/dex.proto | 4 ++++ 7 files changed, 37 insertions(+), 1 deletion(-) diff --git a/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs b/crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs index c2c1f4df906c5e549e4bf4ff1514fc587b551e16..d50e1bd8818eb8b4d521b590c16703d42bad4e23 100644 GIT binary patch delta 20237 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 bfV?~$!Ml3V^q_EN)y%3n>1V%bz3+bkz^~Z- diff --git a/crates/core/app/src/params/change.rs b/crates/core/app/src/params/change.rs index 9f96c61478..78ff79d57d 100644 --- a/crates/core/app/src/params/change.rs +++ b/crates/core/app/src/params/change.rs @@ -25,7 +25,6 @@ impl AppParameters { new.check_valid()?; // TODO: move the checks below into their respective components. // Tracked by #3593 - let AppParameters { chain_id, community_pool_params: @@ -75,6 +74,7 @@ impl AppParameters { is_enabled: _, fixed_candidates: _, max_hops: _, + max_positions_per_pair: _, }, // IMPORTANT: Don't use `..` here! We want to ensure every single field is verified! } = self; @@ -169,6 +169,7 @@ impl AppParameters { is_enabled: _, fixed_candidates: _, max_hops: _, + max_positions_per_pair: _, }, // IMPORTANT: Don't use `..` here! We want to ensure every single field is verified! } = self; @@ -244,6 +245,7 @@ impl AppParameters { *min_validator_stake >= 1_000_000u128.into(), "the minimum validator stake must be at least 1penumbra", ), + // TODO(erwan): add a `max_positions_per_pair` check ]) } diff --git a/crates/core/component/dex/src/params.rs b/crates/core/component/dex/src/params.rs index 32233c251b..29ae3b2649 100644 --- a/crates/core/component/dex/src/params.rs +++ b/crates/core/component/dex/src/params.rs @@ -10,6 +10,7 @@ pub struct DexParameters { pub is_enabled: bool, pub fixed_candidates: Vec, pub max_hops: u32, + pub max_positions_per_pair: u32, } impl DomainType for DexParameters { @@ -28,6 +29,7 @@ impl TryFrom for DexParameters { .map(|id| id.try_into()) .collect::>()?, max_hops: msg.max_hops, + max_positions_per_pair: msg.max_positions_per_pair, }) } } @@ -42,6 +44,7 @@ impl From for pb::DexParameters { .map(Into::into) .collect(), max_hops: params.max_hops, + max_positions_per_pair: params.max_positions_per_pair, } } } @@ -63,6 +66,8 @@ impl Default for DexParameters { cache.get_unit("test_btc").unwrap().id(), ], max_hops: 4, + // TODO(erwan): setting this to 10 for testing. + max_positions_per_pair: 10, } } } diff --git a/crates/proto/src/gen/penumbra.core.component.dex.v1.rs b/crates/proto/src/gen/penumbra.core.component.dex.v1.rs index 316658f996..118449a51b 100644 --- a/crates/proto/src/gen/penumbra.core.component.dex.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.dex.v1.rs @@ -1509,6 +1509,11 @@ pub struct DexParameters { /// The number of hops to traverse while routing from A to B. #[prost(uint32, tag = "3")] pub max_hops: u32, + /// The maximum number of positions per trading pair. + /// If this number is exceeded, positions with the least + /// inventory get evicted from the DEX. + #[prost(uint32, tag = "4")] + pub max_positions_per_pair: u32, } impl ::prost::Name for DexParameters { const NAME: &'static str = "DexParameters"; diff --git a/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs index b7294296fc..a82e8bc664 100644 --- a/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs @@ -1052,6 +1052,9 @@ impl serde::Serialize for DexParameters { if self.max_hops != 0 { len += 1; } + if self.max_positions_per_pair != 0 { + len += 1; + } let mut struct_ser = serializer.serialize_struct("penumbra.core.component.dex.v1.DexParameters", len)?; if self.is_enabled { struct_ser.serialize_field("isEnabled", &self.is_enabled)?; @@ -1062,6 +1065,9 @@ impl serde::Serialize for DexParameters { if self.max_hops != 0 { struct_ser.serialize_field("maxHops", &self.max_hops)?; } + if self.max_positions_per_pair != 0 { + struct_ser.serialize_field("maxPositionsPerPair", &self.max_positions_per_pair)?; + } struct_ser.end() } } @@ -1078,6 +1084,8 @@ impl<'de> serde::Deserialize<'de> for DexParameters { "fixedCandidates", "max_hops", "maxHops", + "max_positions_per_pair", + "maxPositionsPerPair", ]; #[allow(clippy::enum_variant_names)] @@ -1085,6 +1093,7 @@ impl<'de> serde::Deserialize<'de> for DexParameters { IsEnabled, FixedCandidates, MaxHops, + MaxPositionsPerPair, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -1110,6 +1119,7 @@ impl<'de> serde::Deserialize<'de> for DexParameters { "isEnabled" | "is_enabled" => Ok(GeneratedField::IsEnabled), "fixedCandidates" | "fixed_candidates" => Ok(GeneratedField::FixedCandidates), "maxHops" | "max_hops" => Ok(GeneratedField::MaxHops), + "maxPositionsPerPair" | "max_positions_per_pair" => Ok(GeneratedField::MaxPositionsPerPair), _ => Ok(GeneratedField::__SkipField__), } } @@ -1132,6 +1142,7 @@ impl<'de> serde::Deserialize<'de> for DexParameters { let mut is_enabled__ = None; let mut fixed_candidates__ = None; let mut max_hops__ = None; + let mut max_positions_per_pair__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::IsEnabled => { @@ -1154,6 +1165,14 @@ impl<'de> serde::Deserialize<'de> for DexParameters { Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) ; } + GeneratedField::MaxPositionsPerPair => { + if max_positions_per_pair__.is_some() { + return Err(serde::de::Error::duplicate_field("maxPositionsPerPair")); + } + max_positions_per_pair__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -1163,6 +1182,7 @@ impl<'de> serde::Deserialize<'de> for DexParameters { is_enabled: is_enabled__.unwrap_or_default(), fixed_candidates: fixed_candidates__.unwrap_or_default(), max_hops: max_hops__.unwrap_or_default(), + max_positions_per_pair: max_positions_per_pair__.unwrap_or_default(), }) } } diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index d2be9305bfb238b8d714e9072848c7084214b0c8..5c5a38007f0f3ea943adfb4e1452c7cee9443fae 100644 GIT binary patch delta 20509 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 Date: Thu, 4 Apr 2024 14:59:17 -0700 Subject: [PATCH 070/168] pcli: bump colored_json to 4.1 (#4159) ## Describe your changes Bump the version of colored_json used by pcli to 4.1. ## Issue ticket number and link ## Checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > This is a dependency upgrade that mainly just removes subdependencies and bumps the edition to 2021. --- Cargo.lock | 14 +++++++++----- crates/bin/pcli/Cargo.toml | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b65a4d8260..5f2b44fcfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1440,15 +1440,13 @@ dependencies = [ [[package]] name = "colored_json" -version = "2.1.0" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd32eb54d016e203b7c2600e3a7802c75843a92e38ccc4869aefeca21771a64" +checksum = "79cff32df5cfea75e6484eeff0b4e48ad3977fb6582676a7862b3590dddc7a87" dependencies = [ - "ansi_term", - "atty", - "libc", "serde", "serde_json", + "yansi", ] [[package]] @@ -9026,6 +9024,12 @@ dependencies = [ "rustix 0.38.31", ] +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "yasna" version = "0.5.2" diff --git a/crates/bin/pcli/Cargo.toml b/crates/bin/pcli/Cargo.toml index 6639a4254f..415ac3f041 100644 --- a/crates/bin/pcli/Cargo.toml +++ b/crates/bin/pcli/Cargo.toml @@ -40,7 +40,7 @@ bytes = {workspace = true} camino = {workspace = true} clap = {workspace = true, features = ["derive", "env"]} colored = "2.1.0" -colored_json = "2.1" +colored_json = "4.1" comfy-table = "5" decaf377 = {workspace = true, default-features = true} decaf377-rdsa = {workspace = true} From 8b06546af43bf073fd99f3f9d82b8afb51872489 Mon Sep 17 00:00:00 2001 From: noot <36753753+noot@users.noreply.github.com> Date: Thu, 4 Apr 2024 18:23:53 -0400 Subject: [PATCH 071/168] tower-trace: add support for v038 (#4160) ## Describe your changes implement support for ABCI v038 for the tower-trace util crate. required for eventual upgrade to ABCI v038 :) ## Issue ticket number and link n/a ## Checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: it only affects the tower-trace util crate --- crates/util/tower-trace/src/lib.rs | 5 + .../util/tower-trace/src/v038/request_ext.rs | 124 ++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 crates/util/tower-trace/src/v038/request_ext.rs diff --git a/crates/util/tower-trace/src/lib.rs b/crates/util/tower-trace/src/lib.rs index 47e90f1a03..5e60baf249 100644 --- a/crates/util/tower-trace/src/lib.rs +++ b/crates/util/tower-trace/src/lib.rs @@ -28,3 +28,8 @@ pub mod v037 { mod request_ext; pub use request_ext::RequestExt; } + +pub mod v038 { + mod request_ext; + pub use request_ext::RequestExt; +} diff --git a/crates/util/tower-trace/src/v038/request_ext.rs b/crates/util/tower-trace/src/v038/request_ext.rs new file mode 100644 index 0000000000..a3bd9046c5 --- /dev/null +++ b/crates/util/tower-trace/src/v038/request_ext.rs @@ -0,0 +1,124 @@ +use sha2::{Digest, Sha256}; +use tendermint::v0_38::abci::request::{CheckTx, FinalizeBlock, InitChain, Query, Request}; +use tendermint::v0_38::abci::request::{ + ConsensusRequest, InfoRequest, MempoolRequest, SnapshotRequest, +}; +use tendermint::v0_38::abci::request::{ExtendVote, VerifyVoteExtension}; +use tendermint::v0_38::abci::request::{PrepareProposal, ProcessProposal}; +use tracing::error_span; + +pub trait RequestExt { + /// Create a [`tracing::Span`] for this request, including the request name + /// and some relevant context (but not including the entire request data). + fn create_span(&self) -> tracing::Span; +} + +impl RequestExt for ConsensusRequest { + fn create_span(&self) -> tracing::Span { + // Create a parent "abci" span. All of these spans are at error level, so they're always recorded. + let p = error_span!("abci"); + match self { + ConsensusRequest::FinalizeBlock(FinalizeBlock { hash, height, .. }) => { + error_span!(parent: &p, "FinalizeBlock", height = ?height, hash = ?hex::encode(hash.as_ref())) + } + ConsensusRequest::Commit => error_span!(parent: &p, "Commit"), + ConsensusRequest::InitChain(InitChain { chain_id, .. }) => { + error_span!(parent: &p, "InitChain", ?chain_id) + } + ConsensusRequest::PrepareProposal(PrepareProposal { height, .. }) => { + error_span!(parent: &p, "PrepareProposal", ?height) + } + ConsensusRequest::ProcessProposal(ProcessProposal { height, .. }) => { + error_span!(parent: &p, "ProcessProposal", ?height) + } + ConsensusRequest::ExtendVote(ExtendVote { hash, height, .. }) => { + error_span!(parent: &p, "ExtendVote", height = ?height, hash = ?hex::encode(hash.as_ref())) + } + ConsensusRequest::VerifyVoteExtension(VerifyVoteExtension { hash, height, .. }) => { + error_span!(parent: &p, "VerifyVoteExtension", height = ?height, hash = ?hex::encode(hash.as_ref())) + } + } + } +} + +impl RequestExt for MempoolRequest { + fn create_span(&self) -> tracing::Span { + // Create a parent "abci" span. All of these spans are at error level, so they're always recorded. + let p = error_span!("abci"); + match self { + MempoolRequest::CheckTx(CheckTx { kind, tx }) => { + error_span!(parent: &p, "CheckTx", ?kind, txid = ?hex::encode(Sha256::digest(tx.as_ref()))) + } + } + } +} + +impl RequestExt for InfoRequest { + fn create_span(&self) -> tracing::Span { + // Create a parent "abci" span. All of these spans are at error level, so they're always recorded. + let p = error_span!("abci"); + match self { + InfoRequest::Info(_) => error_span!(parent: &p, "Info"), + InfoRequest::Query(Query { + path, + height, + prove, + .. + }) => { + error_span!(parent: &p, "Query", ?path, ?height, prove) + } + InfoRequest::Echo(_) => error_span!(parent: &p, "Echo"), + } + } +} + +impl RequestExt for SnapshotRequest { + fn create_span(&self) -> tracing::Span { + // Create a parent "abci" span. All of these spans are at error level, so they're always recorded. + let p = error_span!("abci"); + match self { + SnapshotRequest::ListSnapshots => error_span!(parent: &p, "ListSnapshots"), + SnapshotRequest::OfferSnapshot(_) => error_span!(parent: &p, "OfferSnapshot"), + SnapshotRequest::LoadSnapshotChunk(_) => error_span!(parent: &p, "LoadSnapshotChunk"), + SnapshotRequest::ApplySnapshotChunk(_) => error_span!(parent: &p, "ApplySnapshotChunk"), + } + } +} + +impl RequestExt for Request { + fn create_span(&self) -> tracing::Span { + // Create a parent "abci" span. All of these spans are at error level, so they're always recorded. + let p = error_span!("abci"); + match self { + Request::Info(_) => error_span!(parent: &p, "Info"), + Request::Query(Query { + path, + height, + prove, + .. + }) => { + error_span!(parent: &p, "Query", ?path, ?height, prove) + } + Request::CheckTx(CheckTx { kind, tx }) => { + error_span!(parent: &p, "CheckTx", ?kind, txid = ?hex::encode(Sha256::digest(tx.as_ref()))) + } + Request::FinalizeBlock(FinalizeBlock { hash, height, .. }) => { + error_span!(parent: &p, "FinalizeBlock", height = ?height, hash = ?hex::encode(hash.as_ref())) + } + Request::Commit => error_span!(parent: &p, "Commit"), + Request::InitChain(InitChain { chain_id, .. }) => { + error_span!(parent: &p, "InitChain", ?chain_id) + } + Request::Flush => error_span!(parent: &p, "Flush"), + Request::Echo(_) => error_span!(parent: &p, "Echo"), + Request::ListSnapshots => error_span!(parent: &p, "ListSnapshots"), + Request::OfferSnapshot(_) => error_span!(parent: &p, "OfferSnapshot"), + Request::LoadSnapshotChunk(_) => error_span!(parent: &p, "LoadSnapshotChunk"), + Request::ApplySnapshotChunk(_) => error_span!(parent: &p, "ApplySnapshotChunk"), + Request::PrepareProposal(_) => error_span!(parent: &p, "PrepareProposal"), + Request::ProcessProposal(_) => error_span!(parent: &p, "ProcessProposal"), + Request::ExtendVote(_) => error_span!(parent: &p, "ExtendVote"), + Request::VerifyVoteExtension(_) => error_span!(parent: &p, "VerifyVoteExtension"), + } + } +} From 4fce95321275e52d61ddd0fa966cc3716b483bbb Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Sun, 7 Apr 2024 15:52:35 -0400 Subject: [PATCH 072/168] penumbra: remove `Builder` factory (#4169) ## Describe your changes We use named tasks in `pd` and `cnidarium` via `tokio::task::Builder`. This is a great feature that improves `tokio-console` rendering and has proved valuable in the past. However, since tracing integration in tokio is still unstable, using this requires a special cargo configuration to run rustc with a special `tokio_unstable` flag. We have received a lot of direct feedback that this is 1/ surprising 2/ cumbersome, and since it seem like there are no immediate plans to stabilize tracing in tokio, we are removing it for now. ## Checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > Internal only. --- .cargo/config | 4 +- crates/bin/pd/src/main.rs | 12 +- crates/cnidarium/src/snapshot.rs | 253 +++++++++++-------------- crates/cnidarium/src/storage.rs | 91 +++++---- crates/cnidarium/src/store/substore.rs | 7 +- 5 files changed, 165 insertions(+), 202 deletions(-) diff --git a/.cargo/config b/.cargo/config index 404305d73c..c5d8c8e32e 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,3 +1,5 @@ [build] # Enable Tokio's `tracing` support for `tokio-console` -rustflags = ["--cfg", "tokio_unstable"] \ No newline at end of file +# rustflags = ["--cfg", "tokio_unstable"] +# Note(erwan): We decided to disable it for the time being, +# I'm keeping this around to be able to reactivate it on a whim. diff --git a/crates/bin/pd/src/main.rs b/crates/bin/pd/src/main.rs index e8dd635958..48bdec77c3 100644 --- a/crates/bin/pd/src/main.rs +++ b/crates/bin/pd/src/main.rs @@ -120,10 +120,9 @@ async fn main() -> anyhow::Result<()> { "starting pd" ); - let abci_server = tokio::task::Builder::new() - .name("abci_server") - .spawn(penumbra_app::server::new(storage.clone()).listen_tcp(abci_bind)) - .expect("failed to spawn abci server"); + let abci_server = tokio::task::spawn( + penumbra_app::server::new(storage.clone()).listen_tcp(abci_bind), + ); let grpc_server = penumbra_app::rpc::router(&storage, cometbft_addr, enable_expensive_rpc)?; @@ -148,10 +147,7 @@ async fn main() -> anyhow::Result<()> { // resolver if auto-https has been enabled. macro_rules! spawn_grpc_server { ($server:expr) => { - tokio::task::Builder::new() - .name("grpc_server") - .spawn($server.serve(make_svc)) - .expect("failed to spawn grpc server") + tokio::task::spawn($server.serve(make_svc)) }; } let grpc_server = axum_server::bind(grpc_bind); diff --git a/crates/cnidarium/src/snapshot.rs b/crates/cnidarium/src/snapshot.rs index c59a652a12..7463db16a2 100644 --- a/crates/cnidarium/src/snapshot.rs +++ b/crates/cnidarium/src/snapshot.rs @@ -81,13 +81,11 @@ impl Snapshot { db: db.clone(), }; - let (substore_value, substore_commitment_proof) = tokio::task::Builder::new() - .name("Snapshot::get_with_proof") - .spawn_blocking({ - let span = span.clone(); - move || span.in_scope(|| substore.get_with_proof(substore_key_bytes)) - })? - .await??; + let (substore_value, substore_commitment_proof) = tokio::task::spawn_blocking({ + let span = span.clone(); + move || span.in_scope(|| substore.get_with_proof(substore_key_bytes)) + }) + .await??; proofs.push(substore_commitment_proof); @@ -104,13 +102,11 @@ impl Snapshot { db, }; - let (_, main_commitment_proof) = tokio::task::Builder::new() - .name("Snapshot::get_with_proof") - .spawn_blocking({ - let span = span.clone(); - move || span.in_scope(|| mainstore.get_with_proof(key_to_substore_root.into())) - })? - .await??; + let (_, main_commitment_proof) = tokio::task::spawn_blocking({ + let span = span.clone(); + move || span.in_scope(|| mainstore.get_with_proof(key_to_substore_root.into())) + }) + .await??; proofs.push(main_commitment_proof); } @@ -172,10 +168,7 @@ impl Snapshot { "fetching root hash for substore" ); - tokio::task::Builder::new() - .name("Snapshot::prefix_root_hash") - .spawn_blocking(move || span.in_scope(|| substore.root_hash()))? - .await? + tokio::task::spawn_blocking(move || span.in_scope(|| substore.root_hash())).await? } pub async fn root_hash(&self) -> Result { @@ -221,21 +214,15 @@ impl StateRead for Snapshot { }; let key_hash = jmt::KeyHash::with::(key); - crate::future::SnapshotFuture( - tokio::task::Builder::new() - .name("Snapshot::get_raw") - .spawn_blocking(move || { - span.in_scope(|| { - let _start = std::time::Instant::now(); - let rsp = substore.get_jmt(key_hash); - #[cfg(feature = "metrics")] - metrics::histogram!(metrics::STORAGE_GET_RAW_DURATION) - .record(_start.elapsed()); - rsp - }) - }) - .expect("spawning threads is possible"), - ) + crate::future::SnapshotFuture(tokio::task::spawn_blocking(move || { + span.in_scope(|| { + let _start = std::time::Instant::now(); + let rsp = substore.get_jmt(key_hash); + #[cfg(feature = "metrics")] + metrics::histogram!(metrics::STORAGE_GET_RAW_DURATION).record(_start.elapsed()); + rsp + }) + })) } /// Fetch a key from nonverifiable storage. @@ -258,26 +245,21 @@ impl StateRead for Snapshot { }; let key: Vec = key.to_vec(); - crate::future::SnapshotFuture( - tokio::task::Builder::new() - .name("Snapshot::nonverifiable_get_raw") - .spawn_blocking(move || { - span.in_scope(|| { - let _start = std::time::Instant::now(); - - let cf_nonverifiable = substore.config.cf_nonverifiable(&substore.db); - let rsp = substore - .rocksdb_snapshot - .get_cf(cf_nonverifiable, key) - .map_err(Into::into); - #[cfg(feature = "metrics")] - metrics::histogram!(metrics::STORAGE_NONCONSENSUS_GET_RAW_DURATION) - .record(_start.elapsed()); - rsp - }) - }) - .expect("spawning threads is possible"), - ) + crate::future::SnapshotFuture(tokio::task::spawn_blocking(move || { + span.in_scope(|| { + let _start = std::time::Instant::now(); + + let cf_nonverifiable = substore.config.cf_nonverifiable(&substore.db); + let rsp = substore + .rocksdb_snapshot + .get_cf(cf_nonverifiable, key) + .map_err(Into::into); + #[cfg(feature = "metrics")] + metrics::histogram!(metrics::STORAGE_NONCONSENSUS_GET_RAW_DURATION) + .record(_start.elapsed()); + rsp + }) + })) } /// Returns a stream of all key-value pairs with the given prefix. @@ -309,36 +291,33 @@ impl StateRead for Snapshot { // Since the JMT keys are hashed, we can't use a prefix iterator directly. // We need to first prefix range the key preimages column family, then use the hashed matches to fetch the values // from the JMT column family. - tokio::task::Builder::new() - .name("Snapshot::prefix_raw") - .spawn_blocking(move || { - span.in_scope(|| { - let cf_jmt_keys = substore.config.cf_jmt_keys(&substore.db); - let jmt_keys_iterator = - substore - .rocksdb_snapshot - .iterator_cf_opt(cf_jmt_keys, options, mode); - - for tuple in jmt_keys_iterator { - // For each key that matches the prefix, fetch the value from the JMT column family. - let (key_preimage, _) = tuple?; - - let k = std::str::from_utf8(key_preimage.as_ref()) - .expect("saved jmt keys are utf-8 strings") - .to_string(); - - let key_hash = jmt::KeyHash::with::(k.as_bytes()); - - let v = substore - .get_jmt(key_hash)? - .expect("keys in jmt_keys should have a corresponding value in jmt"); - - tx_prefix_item.blocking_send(Ok((k, v)))?; - } - anyhow::Ok(()) - }) + tokio::task::spawn_blocking(move || { + span.in_scope(|| { + let cf_jmt_keys = substore.config.cf_jmt_keys(&substore.db); + let jmt_keys_iterator = + substore + .rocksdb_snapshot + .iterator_cf_opt(cf_jmt_keys, options, mode); + + for tuple in jmt_keys_iterator { + // For each key that matches the prefix, fetch the value from the JMT column family. + let (key_preimage, _) = tuple?; + + let k = std::str::from_utf8(key_preimage.as_ref()) + .expect("saved jmt keys are utf-8 strings") + .to_string(); + + let key_hash = jmt::KeyHash::with::(k.as_bytes()); + + let v = substore + .get_jmt(key_hash)? + .expect("keys in jmt_keys should have a corresponding value in jmt"); + + tx_prefix_item.blocking_send(Ok((k, v)))?; + } + anyhow::Ok(()) }) - .expect("should be able to spawn_blocking"); + }); tokio_stream::wrappers::ReceiverStream::new(rx_prefix_query) } @@ -371,27 +350,23 @@ impl StateRead for Snapshot { let mode = rocksdb::IteratorMode::Start; let (tx_prefix_keys, rx_prefix_keys) = mpsc::channel(10); - tokio::task::Builder::new() - .name("Snapshot::prefix_keys") - .spawn_blocking(move || { - span.in_scope(|| { - let cf_jmt_keys = substore.config.cf_jmt_keys(&substore.db); - let iter = - substore - .rocksdb_snapshot - .iterator_cf_opt(cf_jmt_keys, options, mode); - - for key_and_keyhash in iter { - let (raw_preimage, _) = key_and_keyhash?; - let preimage = std::str::from_utf8(raw_preimage.as_ref()) - .expect("saved jmt keys are utf-8 strings") - .to_string(); - tx_prefix_keys.blocking_send(Ok(preimage))?; - } - anyhow::Ok(()) - }) + tokio::task::spawn_blocking(move || { + span.in_scope(|| { + let cf_jmt_keys = substore.config.cf_jmt_keys(&substore.db); + let iter = substore + .rocksdb_snapshot + .iterator_cf_opt(cf_jmt_keys, options, mode); + + for key_and_keyhash in iter { + let (raw_preimage, _) = key_and_keyhash?; + let preimage = std::str::from_utf8(raw_preimage.as_ref()) + .expect("saved jmt keys are utf-8 strings") + .to_string(); + tx_prefix_keys.blocking_send(Ok(preimage))?; + } + anyhow::Ok(()) }) - .expect("should be able to spawn_blocking"); + }); tokio_stream::wrappers::ReceiverStream::new(rx_prefix_keys) } @@ -421,23 +396,20 @@ impl StateRead for Snapshot { let (tx_prefix_query, rx_prefix_query) = mpsc::channel(10); - tokio::task::Builder::new() - .name("Snapshot::nonverifiable_prefix_raw") - .spawn_blocking(move || { - span.in_scope(|| { - let cf_nonverifiable = substore.config.cf_nonverifiable(&substore.db); - let iter = - substore - .rocksdb_snapshot - .iterator_cf_opt(cf_nonverifiable, options, mode); - for i in iter { - let (key, value) = i?; - tx_prefix_query.blocking_send(Ok((key.into(), value.into())))?; - } - anyhow::Ok(()) - }) + tokio::task::spawn_blocking(move || { + span.in_scope(|| { + let cf_nonverifiable = substore.config.cf_nonverifiable(&substore.db); + let iter = + substore + .rocksdb_snapshot + .iterator_cf_opt(cf_nonverifiable, options, mode); + for i in iter { + let (key, value) = i?; + tx_prefix_query.blocking_send(Ok((key.into(), value.into())))?; + } + anyhow::Ok(()) }) - .expect("should be able to spawn_blocking"); + }); tokio_stream::wrappers::ReceiverStream::new(rx_prefix_query) } @@ -512,32 +484,29 @@ impl StateRead for Snapshot { let prefix = prefix.to_vec(); let (tx, rx) = mpsc::channel::, Vec)>>(10); - tokio::task::Builder::new() - .name("Snapshot::nonverifiable_range_raw") - .spawn_blocking(move || { - span.in_scope(|| { - let cf_nonverifiable = substore.config.cf_nonverifiable(&substore.db); - let iter = - substore - .rocksdb_snapshot - .iterator_cf_opt(cf_nonverifiable, options, mode); - - for i in iter { - let (key, value) = i?; - - // This is a bit of a hack, but RocksDB doesn't let us express the "prefixed range-queries", - // that we want to support. In particular, we want to be able to do a prefix query that starts - // at a particular key, and does not have an upper bound. Since we can't create an iterator that - // cover this range, we have to filter out the keys that don't match the prefix. - if !prefix.is_empty() && !key.starts_with(&prefix) { - break; - } - tx.blocking_send(Ok((key.into(), value.into())))?; + tokio::task::spawn_blocking(move || { + span.in_scope(|| { + let cf_nonverifiable = substore.config.cf_nonverifiable(&substore.db); + let iter = + substore + .rocksdb_snapshot + .iterator_cf_opt(cf_nonverifiable, options, mode); + + for i in iter { + let (key, value) = i?; + + // This is a bit of a hack, but RocksDB doesn't let us express the "prefixed range-queries", + // that we want to support. In particular, we want to be able to do a prefix query that starts + // at a particular key, and does not have an upper bound. Since we can't create an iterator that + // cover this range, we have to filter out the keys that don't match the prefix. + if !prefix.is_empty() && !key.starts_with(&prefix) { + break; } - Ok::<(), anyhow::Error>(()) - }) + tx.blocking_send(Ok((key.into(), value.into())))?; + } + Ok::<(), anyhow::Error>(()) }) - .expect("should be able to spawn_blocking"); + }); Ok(tokio_stream::wrappers::ReceiverStream::new(rx)) } diff --git a/crates/cnidarium/src/storage.rs b/crates/cnidarium/src/storage.rs index 350ae8f86d..c1c7bed71f 100644 --- a/crates/cnidarium/src/storage.rs +++ b/crates/cnidarium/src/storage.rs @@ -52,51 +52,49 @@ impl Storage { let span = Span::current(); let db_path = path.clone(); // initializing main storage instance. - let prefixes = tokio::task::Builder::new() - .name("config_rocksdb") - .spawn_blocking(move || { - span.in_scope(|| { - let mut opts = Options::default(); - opts.create_if_missing(true); - opts.create_missing_column_families(true); - tracing::info!(?path, "opening rocksdb config column"); - - // Hack(erwan): RocksDB requires us to specify all the column families - // that we want to use upfront. This is problematic when we are initializing - // a new database, because the call to `DBCommon::list_cf` will fail - // if the database manifest is not found. To work around this, we ignore - // the error and assume that the database is empty. - // Tracked in: https://github.com/rust-rocksdb/rust-rocksdb/issues/608 - let mut columns = DB::list_cf(&opts, path.clone()).unwrap_or_default(); - if columns.is_empty() { - columns.push("config".to_string()); - } - - let db = DB::open_cf(&opts, path, columns).expect("can open database"); - let cf_config = db - .cf_handle("config") - .expect("config column family is created if missing"); - let config_iter = db.iterator_cf(cf_config, rocksdb::IteratorMode::Start); - let mut prefixes = Vec::new(); - tracing::info!("reading prefixes from config column family"); - for i in config_iter { - let (key, _) = i.expect("can read from iterator"); - prefixes.push(String::from_utf8(key.to_vec()).expect("prefix is utf8")); + let prefixes = tokio::task::spawn_blocking(move || { + span.in_scope(|| { + let mut opts = Options::default(); + opts.create_if_missing(true); + opts.create_missing_column_families(true); + tracing::info!(?path, "opening rocksdb config column"); + + // Hack(erwan): RocksDB requires us to specify all the column families + // that we want to use upfront. This is problematic when we are initializing + // a new database, because the call to `DBCommon::list_cf` will fail + // if the database manifest is not found. To work around this, we ignore + // the error and assume that the database is empty. + // Tracked in: https://github.com/rust-rocksdb/rust-rocksdb/issues/608 + let mut columns = DB::list_cf(&opts, path.clone()).unwrap_or_default(); + if columns.is_empty() { + columns.push("config".to_string()); + } + + let db = DB::open_cf(&opts, path, columns).expect("can open database"); + let cf_config = db + .cf_handle("config") + .expect("config column family is created if missing"); + let config_iter = db.iterator_cf(cf_config, rocksdb::IteratorMode::Start); + let mut prefixes = Vec::new(); + tracing::info!("reading prefixes from config column family"); + for i in config_iter { + let (key, _) = i.expect("can read from iterator"); + prefixes.push(String::from_utf8(key.to_vec()).expect("prefix is utf8")); + } + + for prefix in default_prefixes { + if !prefixes.contains(&prefix) { + db.put_cf(cf_config, prefix.as_bytes(), b"") + .expect("can write to db"); + prefixes.push(prefix); } + } - for prefix in default_prefixes { - if !prefixes.contains(&prefix) { - db.put_cf(cf_config, prefix.as_bytes(), b"") - .expect("can write to db"); - prefixes.push(prefix); - } - } - - std::mem::drop(db); - prefixes - }) - })? - .await?; + std::mem::drop(db); + prefixes + }) + }) + .await?; Storage::init(db_path, prefixes).await } @@ -112,9 +110,8 @@ impl Storage { pub async fn init(path: PathBuf, prefixes: Vec) -> Result { let span = Span::current(); - tokio::task::Builder::new() - .name("open_rocksdb") - .spawn_blocking(move || { + tokio::task + ::spawn_blocking(move || { span.in_scope(|| { let mut substore_configs = Vec::new(); tracing::info!("initializing global store config"); @@ -230,7 +227,7 @@ impl Storage { db: shared_db, }))) }) - })? + }) .await? } diff --git a/crates/cnidarium/src/store/substore.rs b/crates/cnidarium/src/store/substore.rs index 9fc6b68521..058ca0441f 100644 --- a/crates/cnidarium/src/store/substore.rs +++ b/crates/cnidarium/src/store/substore.rs @@ -365,9 +365,8 @@ impl SubstoreStorage { ) -> Result<(RootHash, rocksdb::WriteBatch)> { let span = Span::current(); - tokio::task::Builder::new() - .name("Storage::commit_inner_substore") - .spawn_blocking(move || { + tokio::task + ::spawn_blocking(move || { span.in_scope(|| { let jmt = jmt::Sha256Jmt::new(&self.substore_snapshot); let unwritten_changes: Vec<_> = cache @@ -440,7 +439,7 @@ impl SubstoreStorage { Ok((root_hash, write_batch)) }) - })? + }) .await? } } From 65b84989030197894f688d2fb78c2a553fdf1310 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Mon, 8 Apr 2024 11:17:02 -0400 Subject: [PATCH 073/168] dex: track total number of positions per pair (#4167) ## Describe your changes Working towards achieving #4077. This PR adds a `PositionCounter` extension trait which implements a `TradingPair` scoped position counter. I am not totally sold on a `u16` for the counter type, yet, but it's useful for testing anyway. ## Checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > This is consensus-breaking because it introduces a new implicit validation rule: a hard-limit on the number of position opened for each trading pair (via counter overflow). It could also be consensus breaking if there was a bug in the DEX that allowed closed positions to be updated, but my assumption is that it's not the case. --- .../component/action_handler/position/open.rs | 1 - .../src/component/circuit_breaker/value.rs | 2 +- .../core/component/dex/src/component/mod.rs | 2 +- .../dex/src/component/position_counter.rs | 118 ++++++++++++++++++ .../dex/src/component/position_manager.rs | 46 ++++++- crates/core/component/dex/src/state_key.rs | 19 +++ 6 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 crates/core/component/dex/src/component/position_counter.rs diff --git a/crates/core/component/dex/src/component/action_handler/position/open.rs b/crates/core/component/dex/src/component/action_handler/position/open.rs index cdc9dd2995..4f2445292b 100644 --- a/crates/core/component/dex/src/component/action_handler/position/open.rs +++ b/crates/core/component/dex/src/component/action_handler/position/open.rs @@ -13,7 +13,6 @@ use crate::{ impl ActionHandler for PositionOpen { type CheckStatelessContext = (); async fn check_stateless(&self, _context: ()) -> Result<()> { - // TODO(chris, erwan, henry): brainstorm safety on `TradingFunction`. // Check: // + reserves are at most 80 bits wide, // + the trading function coefficients are at most 80 bits wide. diff --git a/crates/core/component/dex/src/component/circuit_breaker/value.rs b/crates/core/component/dex/src/component/circuit_breaker/value.rs index 8f4b6396b2..1824eda1a6 100644 --- a/crates/core/component/dex/src/component/circuit_breaker/value.rs +++ b/crates/core/component/dex/src/component/circuit_breaker/value.rs @@ -227,7 +227,7 @@ mod tests { let position = buy_1; state_tx.index_position_by_price(&position, &position.id()); state_tx - .update_available_liquidity(&position, &None) + .update_available_liquidity(&None, &position) .await .expect("able to update liquidity"); state_tx.put(state_key::position_by_id(&id), position); diff --git a/crates/core/component/dex/src/component/mod.rs b/crates/core/component/dex/src/component/mod.rs index 871bd67748..8a6459ee47 100644 --- a/crates/core/component/dex/src/component/mod.rs +++ b/crates/core/component/dex/src/component/mod.rs @@ -11,6 +11,7 @@ mod arb; pub(crate) mod circuit_breaker; mod dex; mod flow; +pub(crate) mod position_counter; pub(crate) mod position_manager; mod swap_manager; @@ -21,6 +22,5 @@ pub(crate) use circuit_breaker::ValueCircuitBreaker; pub use dex::{Dex, StateReadExt, StateWriteExt}; pub use position_manager::{PositionManager, PositionRead}; pub use swap_manager::SwapManager; - #[cfg(test)] pub(crate) mod tests; diff --git a/crates/core/component/dex/src/component/position_counter.rs b/crates/core/component/dex/src/component/position_counter.rs new file mode 100644 index 0000000000..ed4e755e46 --- /dev/null +++ b/crates/core/component/dex/src/component/position_counter.rs @@ -0,0 +1,118 @@ +use anyhow::bail; +use async_trait::async_trait; +use cnidarium::StateWrite; + +use crate::state_key; +use crate::TradingPair; +use anyhow::Result; + +#[async_trait] +pub(crate) trait PositionCounter: StateWrite { + /// Returns the number of position for a [`TradingPair`]. + /// If there were no counter initialized for a given pair, this default to zero. + async fn get_position_count(&self, trading_pair: &TradingPair) -> u16 { + let path = state_key::internal::counter::num_positions::by_trading_pair(trading_pair); + self.get_position_count_from_key(path).await + } + + async fn get_position_count_from_key(&self, path: [u8; 99]) -> u16 { + let Some(raw_count) = self + .nonverifiable_get_raw(&path) + .await + .expect("no deserialization failure") + else { + return 0; + }; + + // This is safe because we only increment the counter via a [`Self::increase_position_counter`]. + let raw_count: [u8; 2] = raw_count + .try_into() + .expect("position counter is at most two bytes"); + u16::from_be_bytes(raw_count) + } + + /// Increment the number of position for a [`TradingPair`]. + /// Returns the updated total, or an error if overflow occurred. + async fn increment_position_counter(&mut self, trading_pair: &TradingPair) -> Result { + let path = state_key::internal::counter::num_positions::by_trading_pair(trading_pair); + let prev = self.get_position_count_from_key(path).await; + + let Some(new_total) = prev.checked_add(1) else { + bail!("incrementing position counter would overflow") + }; + self.nonverifiable_put_raw(path.to_vec(), new_total.to_be_bytes().to_vec()); + Ok(new_total) + } + + /// Decrement the number of positions for a [`TradingPair`], unless it would underflow. + /// Returns the updated total, or an error if underflow occurred. + async fn decrement_position_counter(&mut self, trading_pair: &TradingPair) -> Result { + let path = state_key::internal::counter::num_positions::by_trading_pair(trading_pair); + let prev = self.get_position_count_from_key(path).await; + + let Some(new_total) = prev.checked_sub(1) else { + bail!("decrementing position counter would underflow") + }; + self.nonverifiable_put_raw(path.to_vec(), new_total.to_be_bytes().to_vec()); + Ok(new_total) + } +} +impl PositionCounter for T {} + +// For some reason, `rust-analyzer` is complaining about used imports. +// Silence the warnings until I find a fix. +#[allow(unused_imports)] +mod tests { + use cnidarium::{StateDelta, TempStorage}; + use penumbra_asset::{asset::REGISTRY, Value}; + + use crate::component::position_counter::PositionCounter; + use crate::TradingPair; + + #[tokio::test] + /// Test that we can detect overflows and that they are handled properly: increment is ignored / no crash. + async fn test_no_overflow() -> anyhow::Result<()> { + let asset_a = REGISTRY.parse_denom("upenumbra").unwrap().id(); + let asset_b = REGISTRY.parse_denom("pizza").unwrap().id(); + let trading_pair = TradingPair::new(asset_a, asset_b); + + let storage = TempStorage::new().await?; + let mut delta = StateDelta::new(storage.latest_snapshot()); + + for i in 0..u16::MAX { + let total = delta.increment_position_counter(&trading_pair).await?; + + anyhow::ensure!( + total == i + 1, + "the total amount should be total={}, found={total}", + i + 1 + ); + } + + assert!(delta + .increment_position_counter(&trading_pair) + .await + .is_err()); + assert_eq!(delta.get_position_count(&trading_pair).await, u16::MAX); + + Ok(()) + } + + #[tokio::test] + /// Test that we can detect underflow and that they are handled properly: decrement is ignored / no crash. + async fn test_no_underflow() -> anyhow::Result<()> { + let asset_a = REGISTRY.parse_denom("upenumbra").unwrap().id(); + let asset_b = REGISTRY.parse_denom("pizza").unwrap().id(); + let trading_pair = TradingPair::new(asset_a, asset_b); + + let storage = TempStorage::new().await?; + let mut delta = StateDelta::new(storage.latest_snapshot()); + + let maybe_total = delta.decrement_position_counter(&trading_pair).await; + assert!(maybe_total.is_err()); + + let counter = delta.get_position_count(&trading_pair).await; + assert_eq!(counter, 0u16); + Ok(()) + } +} diff --git a/crates/core/component/dex/src/component/position_manager.rs b/crates/core/component/dex/src/component/position_manager.rs index 1b27fb573f..c22947c376 100644 --- a/crates/core/component/dex/src/component/position_manager.rs +++ b/crates/core/component/dex/src/component/position_manager.rs @@ -16,6 +16,7 @@ use crate::event; use crate::lp::position::State; use crate::lp::Reserves; use crate::{ + component::position_counter::PositionCounter, component::ValueCircuitBreaker, lp::position::{self, Position}, state_key, DirectedTradingPair, @@ -156,6 +157,17 @@ pub trait PositionManager: StateWrite + PositionRead { prev_state.state ); + // Optimization: skip state update if the position is already closed. + // This can happen if the position was queued for closure and premptively + // closed by the DEX engine during execution (e.g. auto-closing). + if prev_state.state == position::State::Closed { + tracing::debug!( + ?id, + "position is already closed so we can skip state updates" + ); + return Ok(()); + } + let new_state = { let mut new_state = prev_state.clone(); new_state.state = position::State::Closed; @@ -186,6 +198,17 @@ pub trait PositionManager: StateWrite + PositionRead { /// Opens a new position, updating all necessary indexes and checking for /// its nonexistence prior to being opened. + /// + /// # Errors + /// This method returns an error if the position is malformed + /// e.g. it is set to a state other than `Opened` + /// or, it specifies a position identifier already used by another position. + /// + /// An error can also occur if a DEX engine invariant is breached + /// e.g. overflowing the position counter (`u16::MAX`) + /// or, overflowing the value circuit breaker (`u128::MAX`) + /// + /// In any of those cases, we do not want to allow a new position to be opened. #[tracing::instrument(level = "debug", skip_all)] async fn open_position(&mut self, position: position::Position) -> Result<()> { // Double-check that the position is in the `Opened` state @@ -202,6 +225,9 @@ pub trait PositionManager: StateWrite + PositionRead { ); } + // Increase the position counter + self.increment_position_counter(&position.phi.pair).await?; + // Credit the DEX for the inflows from this position. self.vcb_credit(position.reserves_1()).await?; self.vcb_credit(position.reserves_2()).await?; @@ -364,6 +390,8 @@ pub(crate) trait Inner: StateWrite { prev_state: Option, new_state: Position, ) -> Result<()> { + use position::State::*; + tracing::debug!(?prev_state, ?new_state, "updating position state"); let id = new_state.id(); @@ -375,13 +403,25 @@ pub(crate) trait Inner: StateWrite { } // Only index the position's liquidity if it is active. - if new_state.state == position::State::Opened { + if new_state.state == Opened { self.index_position_by_price(&new_state, &id); } + if new_state.state == Closed { + // Make sure that we don't double decrement the position + // counter if a position was queued for closure AND closed + // by the DEX engine. + let is_already_closed = prev_state + .as_ref() + .map_or(false, |old_position| old_position.state == Closed); + if !is_already_closed { + self.decrement_position_counter(&new_state.phi.pair).await?; + } + } + // Update the available liquidity for this position's trading pair. // TODO: refactor and streamline this method while implementing eviction. - self.update_available_liquidity(&new_state, &prev_state) + self.update_available_liquidity(&prev_state, &new_state) .await?; self.put(state_key::position_by_id(&id), new_state); @@ -580,8 +620,8 @@ pub(crate) trait Inner: StateWrite { async fn update_available_liquidity( &mut self, - position: &Position, prev_position: &Option, + position: &Position, ) -> Result<()> { // Since swaps may be performed in either direction, the available liquidity indices // need to be calculated and stored for both the A -> B and B -> A directions. diff --git a/crates/core/component/dex/src/state_key.rs b/crates/core/component/dex/src/state_key.rs index dad76e571e..05e3e85ee3 100644 --- a/crates/core/component/dex/src/state_key.rs +++ b/crates/core/component/dex/src/state_key.rs @@ -84,6 +84,25 @@ pub(crate) mod internal { use super::*; use crate::lp::BareTradingFunction; + pub mod counter { + pub mod num_positions { + use crate::TradingPair; + + pub fn prefix() -> &'static str { + "dex/internal/counter/num_positions/" + } + + pub fn by_trading_pair(trading_pair: &TradingPair) -> [u8; 99] { + let mut key = [0u8; 99]; + let prefix_bytes = prefix().as_bytes(); + let canonical_pair_bytes = trading_pair.to_bytes(); + + key[0..35].copy_from_slice(prefix_bytes); + key[35..99].copy_from_slice(&canonical_pair_bytes); + key + } + } + } /// Find assets with liquidity positions from asset `from`, ordered by price. pub mod routable_assets { use penumbra_asset::asset; From cde5e6678e9c109bb988e38f69a12675ccce567d Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Mon, 8 Apr 2024 15:48:25 -0400 Subject: [PATCH 074/168] summonerd: disable console by default (#4176) ## Describe your changes The `tokio-console` flag for `summonerd` defaults to `"false"` which evaluates to `true`. I'm leaving the console subscriber so that we can activate the unstable flag on a whim, but we should not have it run by default. ## Checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > Internal CLI change --- tools/summonerd/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/summonerd/src/main.rs b/tools/summonerd/src/main.rs index c72f60900e..4307eadbe6 100644 --- a/tools/summonerd/src/main.rs +++ b/tools/summonerd/src/main.rs @@ -87,7 +87,7 @@ operating the orchestration. )] struct Opt { /// Enable Tokio Console support. - #[clap(long, default_value = "false")] + #[clap(long)] tokio_console: bool, /// Command to run. #[clap(subcommand)] From cfc49777d19fa97c0034cec8a29bd6d151ff4cef Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Fri, 5 Apr 2024 11:04:11 -0400 Subject: [PATCH 075/168] =?UTF-8?q?tests(app):=20=F0=9F=94=B6=20add=20gene?= =?UTF-8?q?sis=20content=20setters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit this is some common glue code in integration tests for the staking component. in order to smooth the inspection of component logic that runs at epoch boundaries, the defaults for `AppState`'s inner settings for e.g. epoch duration are overriden to shorter values. as is, this requres stating object literals that are somewhat verbose, even with `..T::default()` elision. this commit adds some small `with_` helpers to allow test cases to manipulate these settings succinctly. --- crates/core/app/src/genesis.rs | 24 ++++++++++++++++ ..._can_define_and_delegate_to_a_validator.rs | 15 ++++------ .../app_can_undelegate_from_a_validator.rs | 28 +++++++------------ ..._uptime_for_validators_only_once_active.rs | 15 ++++------ 4 files changed, 46 insertions(+), 36 deletions(-) diff --git a/crates/core/app/src/genesis.rs b/crates/core/app/src/genesis.rs index 85380713cb..942cd80331 100644 --- a/crates/core/app/src/genesis.rs +++ b/crates/core/app/src/genesis.rs @@ -164,6 +164,30 @@ impl DomainType for AppState { type Proto = pb::GenesisAppState; } +impl Content { + pub fn with_epoch_duration(self, epoch_duration: u64) -> Self { + Self { + sct_content: penumbra_sct::genesis::Content { + sct_params: penumbra_sct::params::SctParameters { epoch_duration }, + }, + ..self + } + } + + pub fn with_unbonding_delay(self, unbonding_delay: u64) -> Self { + Self { + stake_content: penumbra_stake::genesis::Content { + stake_params: penumbra_stake::params::StakeParameters { + unbonding_delay, + ..self.stake_content.stake_params + }, + ..self.stake_content + }, + ..self + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs index 4d59dfab20..e4996bf7b0 100644 --- a/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs +++ b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs @@ -5,7 +5,10 @@ use { anyhow::{anyhow, Context}, cnidarium::TempStorage, decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey}, - penumbra_app::{genesis::AppState, server::consensus::Consensus}, + penumbra_app::{ + genesis::{self, AppState}, + server::consensus::Consensus, + }, penumbra_keys::test_keys, penumbra_mock_client::MockClient, penumbra_mock_consensus::TestNode, @@ -31,14 +34,8 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { let storage = TempStorage::new().await?; // Configure an AppState with slightly shorter epochs than usual. - let app_state = AppState::Content(penumbra_app::genesis::Content { - sct_content: penumbra_sct::genesis::Content { - sct_params: penumbra_sct::params::SctParameters { - epoch_duration: EPOCH_DURATION, - }, - }, - ..Default::default() - }); + let app_state = + AppState::Content(genesis::Content::default().with_epoch_duration(EPOCH_DURATION)); // Start the test node. let mut node = { diff --git a/crates/core/app/tests/app_can_undelegate_from_a_validator.rs b/crates/core/app/tests/app_can_undelegate_from_a_validator.rs index de61fd1132..b4f8beb3e9 100644 --- a/crates/core/app/tests/app_can_undelegate_from_a_validator.rs +++ b/crates/core/app/tests/app_can_undelegate_from_a_validator.rs @@ -1,5 +1,3 @@ -use penumbra_num::fixpoint::U128x128; - mod common; use { @@ -7,10 +5,14 @@ use { anyhow::anyhow, ark_ff::UniformRand, cnidarium::TempStorage, - penumbra_app::{genesis::AppState, server::consensus::Consensus}, + penumbra_app::{ + genesis::{self, AppState}, + server::consensus::Consensus, + }, penumbra_keys::test_keys, penumbra_mock_client::MockClient, penumbra_mock_consensus::TestNode, + penumbra_num::fixpoint::U128x128, penumbra_proto::DomainType, penumbra_sct::component::clock::EpochRead as _, penumbra_stake::{ @@ -58,21 +60,11 @@ async fn app_can_undelegate_from_a_validator() -> anyhow::Result<()> { }; // Configure an AppState with slightly shorter epochs than usual. - let app_state = AppState::Content(penumbra_app::genesis::Content { - sct_content: penumbra_sct::genesis::Content { - sct_params: penumbra_sct::params::SctParameters { - epoch_duration: EPOCH_DURATION, - }, - }, - stake_content: penumbra_stake::genesis::Content { - stake_params: penumbra_stake::params::StakeParameters { - unbonding_delay: UNBONDING_DELAY, - ..Default::default() - }, - ..Default::default() - }, - ..Default::default() - }); + let app_state = AppState::Content( + genesis::Content::default() + .with_epoch_duration(EPOCH_DURATION) + .with_unbonding_delay(UNBONDING_DELAY), + ); // Start the test node. let mut node = { diff --git a/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs b/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs index de687e553c..c72bd5f54f 100644 --- a/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs +++ b/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs @@ -4,7 +4,10 @@ use { self::common::BuilderExt, cnidarium::TempStorage, decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey}, - penumbra_app::{genesis::AppState, server::consensus::Consensus}, + penumbra_app::{ + genesis::{self, AppState}, + server::consensus::Consensus, + }, penumbra_keys::test_keys, penumbra_mock_client::MockClient, penumbra_mock_consensus::TestNode, @@ -31,14 +34,8 @@ async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<( let storage = TempStorage::new().await?; // Configure an AppState with slightly shorter epochs than usual. - let app_state = AppState::Content(penumbra_app::genesis::Content { - sct_content: penumbra_sct::genesis::Content { - sct_params: penumbra_sct::params::SctParameters { - epoch_duration: EPOCH_DURATION, - }, - }, - ..Default::default() - }); + let app_state = + AppState::Content(genesis::Content::default().with_epoch_duration(EPOCH_DURATION)); // Start the test node. let mut node = { From b54493f148bdef07a268f5d6a9bf20506a8ff1bf Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Mon, 8 Apr 2024 09:34:06 -0400 Subject: [PATCH 076/168] =?UTF-8?q?tests(app):=20=F0=9F=8F=83=20add=20fast?= =?UTF-8?q?=20forward=20to=20next=20epoch=20helper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit this adds an extension method that can be used to fast forward to the next epoch. this is a very common part of test logic that exercises the staking component. this allows test cases to invoke an extension method to jump ahead, without needing to import the clock component and write a loop inspecting the current epoch after creating and executing an empty block. --- ..._can_define_and_delegate_to_a_validator.rs | 27 +++------- .../app_can_undelegate_from_a_validator.rs | 13 ++--- ..._uptime_for_validators_only_once_active.rs | 51 +++++-------------- crates/core/app/tests/common/mod.rs | 43 ++++++++++++++++ 4 files changed, 66 insertions(+), 68 deletions(-) diff --git a/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs index e4996bf7b0..fa44dc55af 100644 --- a/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs +++ b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs @@ -1,8 +1,6 @@ -mod common; - use { - self::common::BuilderExt, - anyhow::{anyhow, Context}, + self::common::{BuilderExt, TestNodeExt}, + anyhow::anyhow, cnidarium::TempStorage, decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey}, penumbra_app::{ @@ -22,6 +20,8 @@ use { tracing::{error_span, info, Instrument}, }; +mod common; + /// The length of the [`penumbra_sct`] epoch. /// /// This test relies on many epochs turning over, so we will work with a shorter epoch duration. @@ -55,10 +55,7 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { // Fast forward to the next epoch. let snapshot_start = storage.latest_snapshot(); - node.fast_forward(EPOCH_DURATION) - .instrument(error_span!("fast forwarding test node to second epoch")) - .await - .context("fast forwarding {EPOCH_LENGTH} blocks")?; + node.fast_forward_to_next_epoch(&storage).await?; let snapshot_end = storage.latest_snapshot(); // Retrieve the validator definition from the latest snapshot. @@ -337,12 +334,7 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { }; // Fast forward to the next epoch. - node.fast_forward(EPOCH_DURATION) - .instrument(error_span!( - "fast forwarding test node to epoch after delegation" - )) - .await - .context("fast forwarding {EPOCH_LENGTH} blocks")?; + node.fast_forward_to_next_epoch(&storage).await?; let post_delegate_next_epoch_snapshot = storage.latest_snapshot(); // Show that now, after an epoch and with a delegation, the validator is marked active. @@ -488,12 +480,7 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { }); // Fast forward to the next epoch. - node.fast_forward(EPOCH_DURATION) - .instrument(error_span!( - "fast forwarding test node to epoch after undelegation" - )) - .await - .context("fast forwarding {EPOCH_LENGTH} blocks")?; + node.fast_forward_to_next_epoch(&storage).await?; let post_undelegate_next_epoch_snapshot = storage.latest_snapshot(); // Show that after undelegating, the validator is no longer marked active. diff --git a/crates/core/app/tests/app_can_undelegate_from_a_validator.rs b/crates/core/app/tests/app_can_undelegate_from_a_validator.rs index b4f8beb3e9..a7f972b6e4 100644 --- a/crates/core/app/tests/app_can_undelegate_from_a_validator.rs +++ b/crates/core/app/tests/app_can_undelegate_from_a_validator.rs @@ -1,7 +1,5 @@ -mod common; - use { - self::common::BuilderExt, + self::common::{BuilderExt, TestNodeExt}, anyhow::anyhow, ark_ff::UniformRand, cnidarium::TempStorage, @@ -27,6 +25,8 @@ use { tracing::{error_span, info, Instrument}, }; +mod common; + /// The length of the [`penumbra_sct`] epoch. /// /// This test relies on many epochs turning over, so we will work with a shorter epoch duration. @@ -195,12 +195,7 @@ async fn app_can_undelegate_from_a_validator() -> anyhow::Result<()> { } // Fast forward to the next epoch. - { - let start = get_latest_epoch().await.index; - while get_latest_epoch().await.index < start { - node.block().execute().await?; - } - } + node.fast_forward_to_next_epoch(&storage).await?; // Build a transaction that will now undelegate from the validator. let undelegate_rate = storage diff --git a/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs b/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs index c72bd5f54f..6418dc2220 100644 --- a/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs +++ b/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs @@ -1,7 +1,5 @@ -mod common; - use { - self::common::BuilderExt, + self::common::{BuilderExt, TestNodeExt}, cnidarium::TempStorage, decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey}, penumbra_app::{ @@ -22,6 +20,8 @@ use { tracing::{error_span, info, Instrument}, }; +mod common; + #[tokio::test] async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<()> { /// The length of the [`penumbra_sct`] epoch. @@ -234,19 +234,10 @@ async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<( } // Fast forward to the next epoch. + node.fast_forward_to_next_epoch(&storage).await?; + + // The new validator should now be in the consensus set. { - let get_epoch = || async { storage.latest_snapshot().get_current_epoch().await }; - let start = get_epoch() - .await? - .tap(|start| tracing::info!(?start, "fast forwarding to next epoch")); - let next = loop { - node.block().execute().await?; - let current = get_epoch().await?; - if current != start { - break current; - } - }; - tracing::info!(?start, ?next, "finished fast forwarding to next epoch"); assert_eq!( get_latest_consensus_set().await.len(), 2, @@ -363,19 +354,10 @@ async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<( ); // Fast forward to the next epoch. + node.fast_forward_to_next_epoch(&storage).await?; + + // The validator should no longer be part of the consensus set. { - let get_epoch = || async { storage.latest_snapshot().get_current_epoch().await }; - let start = get_epoch() - .await? - .tap(|start| tracing::info!(?start, "fast forwarding to next epoch")); - let next = loop { - node.block().execute().await?; - let current = get_epoch().await?; - if current != start { - break current; - } - }; - tracing::info!(?start, ?next, "finished fast forwarding to next epoch"); assert_eq!( get_latest_consensus_set().await.len(), 1, @@ -403,19 +385,10 @@ async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<( } // Fast forward to the next epoch. + node.fast_forward_to_next_epoch(&storage).await?; + + // There should only be one validator in the consensus set. { - let get_epoch = || async { storage.latest_snapshot().get_current_epoch().await }; - let start = get_epoch() - .await? - .tap(|start| tracing::info!(?start, "fast forwarding to next epoch")); - let next = loop { - node.block().execute().await?; - let current = get_epoch().await?; - if current != start { - break current; - } - }; - tracing::info!(?start, ?next, "finished fast forwarding to next epoch"); assert_eq!( get_latest_consensus_set().await.len(), 1, diff --git a/crates/core/app/tests/common/mod.rs b/crates/core/app/tests/common/mod.rs index 38f9af1d74..043d3d39f0 100644 --- a/crates/core/app/tests/common/mod.rs +++ b/crates/core/app/tests/common/mod.rs @@ -89,3 +89,46 @@ impl TempStorageExt for TempStorage { self.apply_genesis(Default::default()).await } } + +#[async_trait] +pub trait TestNodeExt: Sized { + async fn fast_forward_to_next_epoch( + &mut self, + storage: &TempStorage, + ) -> anyhow::Result; +} + +#[async_trait] +impl TestNodeExt for TestNode +where + C: tower::Service< + tendermint::v0_37::abci::ConsensusRequest, + Response = tendermint::v0_37::abci::ConsensusResponse, + Error = tower::BoxError, + > + Send + + Clone + + 'static, + C::Future: Send + 'static, + C::Error: Sized, +{ + async fn fast_forward_to_next_epoch( + &mut self, + storage: &TempStorage, + ) -> Result { + use {penumbra_sct::component::clock::EpochRead, tap::Tap}; + + let get_epoch = || async { storage.latest_snapshot().get_current_epoch().await }; + let start = get_epoch() + .await? + .tap(|start| tracing::info!(?start, "fast forwarding to next epoch")); + + loop { + self.block().execute().await?; + let current = get_epoch().await?; + if current != start { + tracing::debug!(end = ?current, ?start, "reached next epoch"); + return Ok(current); + } + } + } +} From ec97c99fa194052ecf0b8a94381b48e0c1ece4b4 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Mon, 8 Apr 2024 11:11:55 -0400 Subject: [PATCH 077/168] =?UTF-8?q?tests(app):=20=F0=9F=94=91=20use=20`val?= =?UTF-8?q?idator=5Fidentity=5Fkeys`=20accessor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit this adds a `ValidatorDataRead::validator_identity_keys()` trait method. frequently, test logic needs to keep track of validators by their identity keys. this led to a bit of common glue code that would get the latest snapshot, the set of all validator definitions, assert that only one validator is present, and then take the identity key of that single validator. now we can just get the keys, bind that single entry to a variable, or return an error if something went wrong. --- ..._can_define_and_delegate_to_a_validator.rs | 15 +++++-------- .../app_can_undelegate_from_a_validator.rs | 17 +++++--------- ...me_for_genesis_validator_missing_blocks.rs | 18 +++++---------- ...me_for_genesis_validator_signing_blocks.rs | 22 +++++++------------ ..._uptime_for_validators_only_once_active.rs | 21 ++++++------------ .../validator_handler/validator_store.rs | 9 ++++++++ 6 files changed, 40 insertions(+), 62 deletions(-) diff --git a/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs index fa44dc55af..a3b26cfa52 100644 --- a/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs +++ b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs @@ -59,17 +59,12 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { let snapshot_end = storage.latest_snapshot(); // Retrieve the validator definition from the latest snapshot. - let existing_validator = match snapshot_end - .validator_definitions() - .tap(|_| info!("getting validator definitions")) + let [existing_validator_id] = storage + .latest_snapshot() + .validator_identity_keys() .await? - .as_slice() - { - [v] => v.clone(), - unexpected => panic!("there should be one validator, got: {unexpected:?}"), - }; - - let existing_validator_id = existing_validator.identity_key; + .try_into() + .map_err(|keys| anyhow::anyhow!("expected one key, got: {keys:?}"))?; // Check that we are now in a new epoch. { diff --git a/crates/core/app/tests/app_can_undelegate_from_a_validator.rs b/crates/core/app/tests/app_can_undelegate_from_a_validator.rs index a7f972b6e4..1f7f2c7573 100644 --- a/crates/core/app/tests/app_can_undelegate_from_a_validator.rs +++ b/crates/core/app/tests/app_can_undelegate_from_a_validator.rs @@ -13,10 +13,7 @@ use { penumbra_num::fixpoint::U128x128, penumbra_proto::DomainType, penumbra_sct::component::clock::EpochRead as _, - penumbra_stake::{ - component::validator_handler::ValidatorDataRead as _, validator::Validator, - UndelegateClaimPlan, - }, + penumbra_stake::{component::validator_handler::ValidatorDataRead as _, UndelegateClaimPlan}, penumbra_transaction::{ memo::MemoPlaintext, plan::MemoPlan, TransactionParameters, TransactionPlan, }, @@ -77,16 +74,12 @@ async fn app_can_undelegate_from_a_validator() -> anyhow::Result<()> { }?; // Retrieve the validator definition from the latest snapshot. - let Validator { identity_key, .. } = match storage + let [identity_key] = storage .latest_snapshot() - .validator_definitions() - .tap(|_| info!("getting validator definitions")) + .validator_identity_keys() .await? - .as_slice() - { - [v] => v.clone(), - unexpected => panic!("there should be one validator, got: {unexpected:?}"), - }; // ..and note the asset id for delegation tokens tied to this validator. + .try_into() + .map_err(|keys| anyhow::anyhow!("expected one key, got: {keys:?}"))?; let delegate_token_id = penumbra_stake::DelegationToken::new(identity_key).id(); // Sync the mock client, using the test wallet's spend key, to the latest snapshot. diff --git a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs index e5a76d2e41..126888c365 100644 --- a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs +++ b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs @@ -6,11 +6,9 @@ use { cnidarium::TempStorage, penumbra_app::{genesis::AppState, server::consensus::Consensus}, penumbra_mock_consensus::TestNode, - penumbra_stake::{ - component::validator_handler::validator_store::ValidatorDataRead, validator::Validator, - }, + penumbra_stake::component::validator_handler::validator_store::ValidatorDataRead, tap::Tap, - tracing::{error_span, info, trace, Instrument}, + tracing::{error_span, trace, Instrument}, }; #[tokio::test] @@ -31,16 +29,12 @@ async fn app_tracks_uptime_for_genesis_validator_missing_blocks() -> anyhow::Res }?; // Retrieve the validator definition from the latest snapshot. - let Validator { identity_key, .. } = match storage + let [identity_key] = storage .latest_snapshot() - .validator_definitions() - .tap(|_| info!("getting validator definitions")) + .validator_identity_keys() .await? - .as_slice() - { - [v] => v.clone(), - unexpected => panic!("there should be one validator, got: {unexpected:?}"), - }; + .try_into() + .map_err(|keys| anyhow::anyhow!("expected one key, got: {keys:?}"))?; let get_uptime = || async { storage .latest_snapshot() diff --git a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs index 7d0e3ba9c4..8c9be9a2d2 100644 --- a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs +++ b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs @@ -1,18 +1,16 @@ -mod common; - use { self::common::BuilderExt, anyhow::Context, cnidarium::TempStorage, penumbra_app::{genesis::AppState, server::consensus::Consensus}, penumbra_mock_consensus::TestNode, - penumbra_stake::{ - component::validator_handler::validator_store::ValidatorDataRead, validator::Validator, - }, + penumbra_stake::component::validator_handler::validator_store::ValidatorDataRead, tap::Tap, - tracing::{error_span, info, Instrument}, + tracing::{error_span, Instrument}, }; +mod common; + #[tokio::test] async fn app_tracks_uptime_for_genesis_validator_missing_blocks() -> anyhow::Result<()> { // Install a test logger, acquire some temporary storage, and start the test node. @@ -31,16 +29,12 @@ async fn app_tracks_uptime_for_genesis_validator_missing_blocks() -> anyhow::Res }?; // Retrieve the validator definition from the latest snapshot. - let Validator { identity_key, .. } = match storage + let [identity_key] = storage .latest_snapshot() - .validator_definitions() - .tap(|_| info!("getting validator definitions")) + .validator_identity_keys() .await? - .as_slice() - { - [v] => v.clone(), - unexpected => panic!("there should be one validator, got: {unexpected:?}"), - }; + .try_into() + .map_err(|keys| anyhow::anyhow!("expected one key, got: {keys:?}"))?; let get_uptime = || async { storage .latest_snapshot() diff --git a/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs b/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs index 6418dc2220..8a4553e786 100644 --- a/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs +++ b/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs @@ -17,7 +17,7 @@ use { }, rand_core::OsRng, tap::Tap, - tracing::{error_span, info, Instrument}, + tracing::{error_span, Instrument}, }; mod common; @@ -70,19 +70,12 @@ async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<( }; // Get the identity key of the genesis validator, before we go further. - // Retrieve the validator definition from the latest snapshot. - let existing_validator_id = { - use penumbra_stake::component::validator_handler::validator_store::ValidatorDataRead; - let validators = &storage - .latest_snapshot() - .validator_definitions() - .tap(|_| info!("getting validator definitions")) - .await?; - match validators.as_slice() { - [Validator { identity_key, .. }] => *identity_key, - unexpected => panic!("there should be one validator, got: {unexpected:?}"), - } - }; + let [existing_validator_id] = storage + .latest_snapshot() + .validator_identity_keys() + .await? + .try_into() + .map_err(|keys| anyhow::anyhow!("expected one key, got: {keys:?}"))?; // To define a validator, we need to define two keypairs: an identity key // for the Penumbra application and a consensus key for cometbft. diff --git a/crates/core/component/stake/src/component/validator_handler/validator_store.rs b/crates/core/component/stake/src/component/validator_handler/validator_store.rs index 89abf01686..3f30895d12 100644 --- a/crates/core/component/stake/src/component/validator_handler/validator_store.rs +++ b/crates/core/component/stake/src/component/validator_handler/validator_store.rs @@ -235,6 +235,15 @@ pub trait ValidatorDataRead: StateRead { .try_collect() .await } + + /// Returns a list of **all** known validators identity keys. + async fn validator_identity_keys(&self) -> Result> { + self.prefix(state_key::validators::definitions::prefix()) + .map_ok(|(_key, validator)| validator) + .map_ok(|validator: Validator| validator.identity_key) + .try_collect() + .await + } } impl ValidatorDataRead for T {} From 5584241eaf9e3d0bf5daf371b04adc610cc837db Mon Sep 17 00:00:00 2001 From: Jesse Pinho Date: Mon, 8 Apr 2024 14:25:17 -0700 Subject: [PATCH 078/168] penumbra: update minifront and node status page (#4175) ## Describe your changes Update to the latest minifront and node-status apps ## Checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > Just frontend changes --- assets/minifront.zip | 4 ++-- assets/node-status.zip | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/minifront.zip b/assets/minifront.zip index c4ad381154..638f41e118 100644 --- a/assets/minifront.zip +++ b/assets/minifront.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f1cc4f47b93cf1aee49f540b291ad0af3c069455197c27dd7f41c176605a1c9 -size 2901211 +oid sha256:d3b380691944f00a4143753b4739e8396c2224b78c7d40ea10060b51455b1b0f +size 2991247 diff --git a/assets/node-status.zip b/assets/node-status.zip index 5113e24913..35ed7c5fac 100644 --- a/assets/node-status.zip +++ b/assets/node-status.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e831bf9ae8896aeac6a177979584248b9295d819255c14054e4f007fd78931a1 -size 2691710 +oid sha256:c7e14302ad4c8d7e3abfc8444018a00a6dd3106945b6e495939697e526823450 +size 2761271 From 34d27f3a5f4cc3649c03a01ee0f63f5cc6c13284 Mon Sep 17 00:00:00 2001 From: plaidfinch Date: Mon, 8 Apr 2024 18:15:38 -0400 Subject: [PATCH 079/168] staking: force the ultimate definition upload to disable a validator (#4178) ## Describe your changes This PR adds a **new consensus rule** which forces validators with `sequence_number = u32::MAX` to be in the state `enabled = false`. This reduces the incentive to steal validator identity signing keys, because it means that it's impossible to "lock" a validator in the `enabled` state; the operator can always permanently disable their validator, cutting off profits for the attacker. ## Issue ticket number and link This is an implementation of issue #4170; see its description for more details about the risk this mitigates. ## Checklist before requesting a review - [X] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > **This is a consensus-breaking change**: it rejects validator definitions which would previously have been accepted by the chain. Co-authored-by: finch --- .../src/component/action_handler/validator_definition.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/core/component/stake/src/component/action_handler/validator_definition.rs b/crates/core/component/stake/src/component/action_handler/validator_definition.rs index 4eb38eb5f1..fde0291007 100644 --- a/crates/core/component/stake/src/component/action_handler/validator_definition.rs +++ b/crates/core/component/stake/src/component/action_handler/validator_definition.rs @@ -35,6 +35,15 @@ impl ActionHandler for validator::Definition { anyhow::bail!("validators can declare at most 8 funding streams") } + // This prevents an attacker who compromises a validator identity signing key from locking + // the validator in an enabled state permanently, instead making it so that the original + // operator always has the option of disabling the validator permanently, regardless of what + // the attacker does. This reduces the incentive to steal compromise validator signing keys, + // because it reduces the expected payoff of such a compromise. + if self.validator.sequence_number == u32::MAX && self.validator.enabled { + anyhow::bail!("validators must be disabled when their lifetime is over") + } + // Then, we check the signature: let definition_bytes = self.validator.encode_to_vec(); VerificationKey::try_from(self.validator.identity_key.0) From f7234c0299b3732a6a7119c13a2eafaa6196bc7a Mon Sep 17 00:00:00 2001 From: Conor Schaefer Date: Fri, 5 Apr 2024 16:02:21 -0700 Subject: [PATCH 080/168] ci: add per-crate checks via cargo-hack Suggested by @erwanor, based on the Astria CI config [0]. The goal is to ensure that our default feature sets actually work, by compiling each crate in the workspace individually. fix: feature-gating across the workspace Satisifes the new CI check by massaging the feature sets. [0] https://github.com/astriaorg/astria/blob/6cc8e2b828f8f5ee65e03c2b3383c4252b4c6b81/.github/workflows/test.yml#L64-L68 --- .github/workflows/rust.yml | 15 +++++++++++++++ crates/core/component/compact-block/Cargo.toml | 1 + .../compact-block/src/component/manager.rs | 1 + crates/misc/measure/Cargo.toml | 2 +- crates/test/mock-consensus/Cargo.toml | 2 +- deployments/scripts/check-crate-feature-sets | 13 +++++++++++++ 6 files changed, 32 insertions(+), 2 deletions(-) create mode 100755 deployments/scripts/check-crate-feature-sets diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 646d7aa973..2fe0fe7f5f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -62,3 +62,18 @@ jobs: - name: Load rust cache uses: astriaorg/buildjet-rust-cache@v2.5.1 - run: cargo fmt --all -- --check + + check: + name: Check all crate features + runs-on: buildjet-8vcpu-ubuntu-2204 + steps: + - uses: actions/checkout@v4 + - name: Install rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Load rust cache + uses: astriaorg/buildjet-rust-cache@v2.5.1 + - name: install cargo-hack + run: cargo install cargo-hack + # Build each crate separately, to validate that the feature-gating is working. + # This is a lighter-weight version of `cargo check-all-features --workspace --release`. + - run: ./deployments/scripts/check-crate-feature-sets diff --git a/crates/core/component/compact-block/Cargo.toml b/crates/core/component/compact-block/Cargo.toml index 5e34f25898..e6d179cafd 100644 --- a/crates/core/component/compact-block/Cargo.toml +++ b/crates/core/component/compact-block/Cargo.toml @@ -12,6 +12,7 @@ component = [ "tokio-stream", "tokio", "penumbra-governance/component", + "penumbra-dex/component", ] # proving-keys = ["penumbra-proof-params/proving-keys"] default = ["std", "component"] diff --git a/crates/core/component/compact-block/src/component/manager.rs b/crates/core/component/compact-block/src/component/manager.rs index 3b84fabb45..7cd8b1f903 100644 --- a/crates/core/component/compact-block/src/component/manager.rs +++ b/crates/core/component/compact-block/src/component/manager.rs @@ -1,6 +1,7 @@ use anyhow::{Context, Result}; use async_trait::async_trait; use cnidarium::StateWrite; +#[cfg(feature = "component")] use penumbra_dex::component::{StateReadExt, SwapManager as _}; use penumbra_fee::component::StateReadExt as _; use penumbra_governance::StateReadExt as _; diff --git a/crates/misc/measure/Cargo.toml b/crates/misc/measure/Cargo.toml index 55ffee98c9..fdecc4446f 100644 --- a/crates/misc/measure/Cargo.toml +++ b/crates/misc/measure/Cargo.toml @@ -20,7 +20,7 @@ penumbra-compact-block = {workspace = true, default-features = false} penumbra-proto = {workspace = true, features = ["rpc"], default-features = true} serde_json = {workspace = true} tokio = {workspace = true, features = ["full"]} -tonic = {workspace = true} +tonic = {workspace = true, features = ["tls"]} tracing = {workspace = true} tracing-subscriber = {workspace = true, features = ["env-filter"]} url = {workspace = true} diff --git a/crates/test/mock-consensus/Cargo.toml b/crates/test/mock-consensus/Cargo.toml index 9a844fad02..8c75ced395 100644 --- a/crates/test/mock-consensus/Cargo.toml +++ b/crates/test/mock-consensus/Cargo.toml @@ -18,6 +18,6 @@ ed25519-consensus = { workspace = true } rand_core = { workspace = true } sha2 = { workspace = true } tap = { workspace = true } -tendermint = { workspace = true } +tendermint = { workspace = true, default-features = true } tower = { workspace = true, features = ["full"] } tracing = { workspace = true } diff --git a/deployments/scripts/check-crate-feature-sets b/deployments/scripts/check-crate-feature-sets new file mode 100755 index 0000000000..b196e97cb6 --- /dev/null +++ b/deployments/scripts/check-crate-feature-sets @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# CI script to verify that each crate in the monorepo builds independently. +# This helps us ensure that the feature-gating for e.g. "component" is +# declared explicitly. +set -euo pipefail + + +if ! hash cargo-hack >/dev/null 2>&1 ; then + >&2 echo "ERROR: cargo-hack not found. Install it with: cargo install cargo-hack" + exit 1 +fi + +cargo hack check --workspace --all-targets --all-features --release From fa7a177eea37ef16ac46e7eeee4ef92631e6f78b Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Mon, 8 Apr 2024 18:27:05 -0400 Subject: [PATCH 081/168] penumbra: completely remove `console-subscriber` --- Cargo.lock | 57 ------------------------------------- Cargo.toml | 1 - crates/bin/pd/Cargo.toml | 1 - crates/bin/pd/src/main.rs | 12 ++------ tools/summonerd/Cargo.toml | 1 - tools/summonerd/src/main.rs | 14 ++------- 6 files changed, 4 insertions(+), 82 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f2b44fcfc..748816d82e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1483,43 +1483,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "console-api" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787" -dependencies = [ - "futures-core", - "prost", - "prost-types", - "tonic", - "tracing-core", -] - -[[package]] -name = "console-subscriber" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7481d4c57092cd1c19dd541b92bdce883de840df30aa5d03fd48a3935c01842e" -dependencies = [ - "console-api", - "crossbeam-channel", - "crossbeam-utils", - "futures-task", - "hdrhistogram", - "humantime", - "prost-types", - "serde", - "serde_json", - "thread_local", - "tokio", - "tokio-stream", - "tonic", - "tracing", - "tracing-core", - "tracing-subscriber 0.3.18", -] - [[package]] name = "const-crc32" version = "1.3.0" @@ -1649,15 +1612,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" -[[package]] -name = "crossbeam-channel" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -2805,10 +2759,7 @@ version = "7.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" dependencies = [ - "base64 0.21.7", "byteorder", - "flate2", - "nom", "num-traits", ] @@ -2992,12 +2943,6 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.28" @@ -4540,7 +4485,6 @@ dependencies = [ "chrono", "clap", "cnidarium", - "console-subscriber", "csv", "decaf377 0.5.0", "decaf377-rdsa", @@ -7599,7 +7543,6 @@ dependencies = [ "camino", "chrono", "clap", - "console-subscriber", "decaf377 0.5.0", "futures", "hex", diff --git a/Cargo.toml b/Cargo.toml index 971a549bb3..a7dc78e149 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,7 +129,6 @@ chrono = { default-features = false, version = "0.4" } clap = { version = "3.2" } cnidarium = { default-features = false, path = "crates/cnidarium" } cnidarium-component = { default-features = false, path = "crates/cnidarium-component" } -console-subscriber = { version = "0.2" } criterion = { version = "0.4" } decaf377 = { default-features = false, version = "0.5" } decaf377-fmd = { path = "crates/crypto/decaf377-fmd" } diff --git a/crates/bin/pd/Cargo.toml b/crates/bin/pd/Cargo.toml index b729d89fd5..b7358d4bf3 100644 --- a/crates/bin/pd/Cargo.toml +++ b/crates/bin/pd/Cargo.toml @@ -44,7 +44,6 @@ bytes = { workspace = true } chrono = { workspace = true, default-features = false, features = ["serde"] } clap = { workspace = true, features = ["derive", "env"] } cnidarium = { workspace = true, features = ["migration", "rpc"], default-features = true } -console-subscriber = { workspace = true } csv = "1.1" decaf377 = { workspace = true, features = ["parallel"], default-features = true } decaf377-rdsa = { workspace = true } diff --git a/crates/bin/pd/src/main.rs b/crates/bin/pd/src/main.rs index 48bdec77c3..e59c4d5fae 100644 --- a/crates/bin/pd/src/main.rs +++ b/crates/bin/pd/src/main.rs @@ -4,7 +4,6 @@ use std::error::Error; use std::io::IsTerminal as _; -use console_subscriber::ConsoleLayer; use metrics_tracing_context::{MetricsLayer, TracingContextLayer}; use metrics_util::layers::Stack; @@ -32,7 +31,7 @@ use url::Url; #[tokio::main] async fn main() -> anyhow::Result<()> { // Validate options immediately. - let Opt { tokio_console, cmd } = ::parse(); + let Opt { cmd } = ::parse(); // Instantiate tracing layers. // The MetricsLayer handles enriching metrics output with labels from tracing spans. @@ -49,14 +48,7 @@ async fn main() -> anyhow::Result<()> { .with(filter_layer) .with(fmt_layer) .with(metrics_layer); - if tokio_console { - // The ConsoleLayer enables collection of data for `tokio-console`. - // The `spawn` call will panic if AddrInUse, so we only spawn if enabled. - let console_layer = ConsoleLayer::builder().with_default_env().spawn(); - registry.with(console_layer).init(); - } else { - registry.init(); - } + registry.init(); tracing::info!(?cmd, version = env!("CARGO_PKG_VERSION"), "running command"); match cmd { diff --git a/tools/summonerd/Cargo.toml b/tools/summonerd/Cargo.toml index 4c34d38064..0a2806f637 100644 --- a/tools/summonerd/Cargo.toml +++ b/tools/summonerd/Cargo.toml @@ -21,7 +21,6 @@ bytes = {workspace = true} camino = {workspace = true} chrono = {workspace = true} clap = {workspace = true, features = ["derive", "env", "color"]} -console-subscriber = {workspace = true} decaf377 = {workspace = true} futures = {workspace = true} hex = {workspace = true} diff --git a/tools/summonerd/src/main.rs b/tools/summonerd/src/main.rs index 4307eadbe6..c051af56af 100644 --- a/tools/summonerd/src/main.rs +++ b/tools/summonerd/src/main.rs @@ -14,7 +14,6 @@ use ark_serialize::CanonicalSerialize; use camino::Utf8Path; use camino::Utf8PathBuf; use clap::Parser; -use console_subscriber::ConsoleLayer; use coordinator::Coordinator; use decaf377::Bls12_377; use metrics_tracing_context::MetricsLayer; @@ -86,9 +85,6 @@ operating the orchestration. version, )] struct Opt { - /// Enable Tokio Console support. - #[clap(long)] - tokio_console: bool, /// Command to run. #[clap(subcommand)] pub cmd: Command, @@ -356,8 +352,6 @@ async fn main() -> Result<()> { // Instantiate tracing layers. // The MetricsLayer handles enriching metrics output with labels from tracing spans. let metrics_layer = MetricsLayer::new(); - // The ConsoleLayer enables collection of data for `tokio-console`. - let console_layer = ConsoleLayer::builder().with_default_env().spawn(); // The `FmtLayer` is used to print to the console. let fmt_layer = tracing_subscriber::fmt::layer() .with_ansi(io::stdout().is_terminal()) @@ -369,16 +363,12 @@ async fn main() -> Result<()> { let opt = Opt::parse(); - // Register the tracing subscribers, conditionally enabling tokio console support + // Register the tracing subscribers. let registry = tracing_subscriber::registry() .with(filter_layer) .with(fmt_layer) .with(metrics_layer); - if opt.tokio_console { - registry.with(console_layer).init(); - } else { - registry.init(); - } + registry.init(); opt.exec().await } From 479966cacb0d501ace987fe4838c58f7fe074fb7 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Mon, 8 Apr 2024 18:36:51 -0400 Subject: [PATCH 082/168] pd: remove console flag --- crates/bin/pd/src/cli.rs | 3 --- crates/bin/pd/src/main.rs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/bin/pd/src/cli.rs b/crates/bin/pd/src/cli.rs index f8ee209d73..63bb65aa96 100644 --- a/crates/bin/pd/src/cli.rs +++ b/crates/bin/pd/src/cli.rs @@ -9,9 +9,6 @@ use { #[derive(Debug, Parser)] #[clap(name = "pd", about = "The Penumbra daemon.", version)] pub struct Opt { - /// Enable Tokio Console support. - #[clap(long)] - pub tokio_console: bool, /// Command to run. #[clap(subcommand)] pub cmd: RootCommand, diff --git a/crates/bin/pd/src/main.rs b/crates/bin/pd/src/main.rs index e59c4d5fae..4fbfeaa91a 100644 --- a/crates/bin/pd/src/main.rs +++ b/crates/bin/pd/src/main.rs @@ -43,7 +43,7 @@ async fn main() -> anyhow::Result<()> { // The `EnvFilter` layer is used to filter events based on `RUST_LOG`. let filter_layer = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?; - // Register the tracing subscribers, conditionally enabling tokio console support + // Register the tracing subscribers. let registry = tracing_subscriber::registry() .with(filter_layer) .with(fmt_layer) From 56c67ca4b708041c87c577d75ce9b6e1ba211347 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Mon, 8 Apr 2024 12:22:52 -0400 Subject: [PATCH 083/168] =?UTF-8?q?mock-consensus:=20=F0=9F=AB=9B=20`two?= =?UTF-8?q?=5Fvalidators`=20adds=20two=20keys?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes #3937. * #3937 * #3588 this adds a `two_validators` method to the test node builder, so that tests may set up a test node that has two validator keys. --- crates/test/mock-consensus/src/builder.rs | 36 ++++++++++++++++++----- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/crates/test/mock-consensus/src/builder.rs b/crates/test/mock-consensus/src/builder.rs index e0fc5bae0b..80c86ca21b 100644 --- a/crates/test/mock-consensus/src/builder.rs +++ b/crates/test/mock-consensus/src/builder.rs @@ -8,7 +8,6 @@ mod init_chain; use { crate::{Keyring, TestNode}, bytes::Bytes, - std::collections::BTreeMap, }; /// A buider, used to prepare and instantiate a new [`TestNode`]. @@ -58,15 +57,38 @@ impl Builder { ); } - // Generate a consensus key. + // Generate a key and place it in the keyring. + let mut keyring = Keyring::new(); + Self::add_key(&mut keyring); + + Self { keyring, ..self } + } + + /// Generates a pair of validator keys. + pub fn two_validators(self) -> Self { + let Self { keyring: prev, .. } = self; + + // Log a warning if we are about to overwrite any existing keys. + if !prev.is_empty() { + tracing::warn!( + count = %prev.len(), + "builder overwriting entries in keyring, this may be a bug!" + ); + } + + // Generate two keys and place them in the keyring. + let mut keyring = Keyring::new(); + Self::add_key(&mut keyring); + Self::add_key(&mut keyring); + + Self { keyring, ..self } + } + + /// Generates consensus keys and places them in the provided keyring. + fn add_key(keyring: &mut Keyring) { let sk = ed25519_consensus::SigningKey::new(rand_core::OsRng); let vk = sk.verification_key(); tracing::trace!(verification_key = ?vk, "generated consensus key"); - - // Place it into the keyring. - let mut keyring = BTreeMap::new(); keyring.insert(vk, sk); - - Self { keyring, ..self } } } From 1593f122941f209d3d47ae473768793dad6f4d3e Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Tue, 9 Apr 2024 08:17:04 -0400 Subject: [PATCH 084/168] dex: add an eviction index based on position reserves (#4172) ## Describe your changes This PR implements an inventory index, together with #4167, this PR works toward #4077 which contain the full eviction mechanism along with the adequate protocol specification update. The state key added: - $\text{I}_{A \rightarrow B} \coloneqq \text{prefix} \Vert BE(R_A) \Vert \text{id}$ - $\text{I}_{B \rightarrow A} \coloneqq \text{prefix} \Vert BE(R_B) \Vert \text{id}$ each corresponding to an index of position `Id`s ordered by reserves (ascending). I plan to immediately follow-up this PR with a proposal to refactor the inner `PositionManager` index implementations. ## Checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > It adds a state key to nonverifiable storage, and absent the actual eviction mechanism, this isn't actually consensus breaking. --- .../dex/src/component/position_counter.rs | 8 +- .../dex/src/component/position_manager.rs | 87 ++++++++++++++----- crates/core/component/dex/src/state_key.rs | 56 +++++++++--- 3 files changed, 114 insertions(+), 37 deletions(-) diff --git a/crates/core/component/dex/src/component/position_counter.rs b/crates/core/component/dex/src/component/position_counter.rs index ed4e755e46..5ccec60cdf 100644 --- a/crates/core/component/dex/src/component/position_counter.rs +++ b/crates/core/component/dex/src/component/position_counter.rs @@ -2,7 +2,7 @@ use anyhow::bail; use async_trait::async_trait; use cnidarium::StateWrite; -use crate::state_key; +use crate::state_key::engine; use crate::TradingPair; use anyhow::Result; @@ -11,7 +11,7 @@ pub(crate) trait PositionCounter: StateWrite { /// Returns the number of position for a [`TradingPair`]. /// If there were no counter initialized for a given pair, this default to zero. async fn get_position_count(&self, trading_pair: &TradingPair) -> u16 { - let path = state_key::internal::counter::num_positions::by_trading_pair(trading_pair); + let path = engine::counter::num_positions::by_trading_pair(trading_pair); self.get_position_count_from_key(path).await } @@ -34,7 +34,7 @@ pub(crate) trait PositionCounter: StateWrite { /// Increment the number of position for a [`TradingPair`]. /// Returns the updated total, or an error if overflow occurred. async fn increment_position_counter(&mut self, trading_pair: &TradingPair) -> Result { - let path = state_key::internal::counter::num_positions::by_trading_pair(trading_pair); + let path = engine::counter::num_positions::by_trading_pair(trading_pair); let prev = self.get_position_count_from_key(path).await; let Some(new_total) = prev.checked_add(1) else { @@ -47,7 +47,7 @@ pub(crate) trait PositionCounter: StateWrite { /// Decrement the number of positions for a [`TradingPair`], unless it would underflow. /// Returns the updated total, or an error if underflow occurred. async fn decrement_position_counter(&mut self, trading_pair: &TradingPair) -> Result { - let path = state_key::internal::counter::num_positions::by_trading_pair(trading_pair); + let path = engine::counter::num_positions::by_trading_pair(trading_pair); let prev = self.get_position_count_from_key(path).await; let Some(new_total) = prev.checked_sub(1) else { diff --git a/crates/core/component/dex/src/component/position_manager.rs b/crates/core/component/dex/src/component/position_manager.rs index c22947c376..0b51ae5718 100644 --- a/crates/core/component/dex/src/component/position_manager.rs +++ b/crates/core/component/dex/src/component/position_manager.rs @@ -12,15 +12,17 @@ use penumbra_num::Amount; use penumbra_proto::DomainType; use penumbra_proto::{StateReadProto, StateWriteProto}; -use crate::event; use crate::lp::position::State; use crate::lp::Reserves; use crate::{ component::position_counter::PositionCounter, component::ValueCircuitBreaker, lp::position::{self, Position}, - state_key, DirectedTradingPair, + state_key::engine, + state_key::eviction_queue, + DirectedTradingPair, }; +use crate::{event, state_key}; const DYNAMIC_ASSET_LIMIT: usize = 10; @@ -47,7 +49,7 @@ pub trait PositionRead: StateRead { &self, pair: &DirectedTradingPair, ) -> Pin> + Send + 'static>> { - let prefix = state_key::internal::price_index::prefix(pair); + let prefix = engine::price_index::prefix(pair); tracing::trace!(prefix = ?EscapedByteSlice(&prefix), "searching for positions by price"); self.nonverifiable_prefix_raw(&prefix) .map(|entry| match entry { @@ -118,7 +120,7 @@ pub trait PositionRead: StateRead { &self, from: &asset::Id, ) -> Pin> + Send + 'static>> { - let prefix = state_key::internal::routable_assets::prefix(from); + let prefix = engine::routable_assets::prefix(from); tracing::trace!(prefix = ?EscapedByteSlice(&prefix), "searching for routable assets by liquidity"); self.nonverifiable_prefix_raw(&prefix) .map(|entry| match entry { @@ -400,11 +402,13 @@ pub(crate) trait Inner: StateWrite { // reserves or the position state might have invalidated them. if let Some(prev_state) = prev_state.as_ref() { self.deindex_position_by_price(&prev_state, &id); + self.deindex_position_by_inventory(&prev_state, &id); } // Only index the position's liquidity if it is active. if new_state.state == Opened { self.index_position_by_price(&new_state, &id); + self.index_position_by_inventory(&new_state, &id); } if new_state.state == Closed { @@ -428,6 +432,55 @@ pub(crate) trait Inner: StateWrite { Ok(()) } + // TODO(erwan): break this out into a `position_manager::inventory_index` module. + fn index_position_by_inventory(&mut self, position: &position::Position, id: &position::Id) { + tracing::debug!("indexing position by inventory"); + let canonical_pair = position.phi.pair; + // A position is bound to an unordered trading pair: A <> B. + // We want to index the position by inventory for each direction: + // A -> B + let pair_ab = DirectedTradingPair::new(canonical_pair.asset_1(), canonical_pair.asset_2()); + let inventory_a = position + .reserves_for(pair_ab.start) + .expect("the directed trading pair is correct"); + let key_ab = eviction_queue::inventory_index::key(&pair_ab, inventory_a, id).to_vec(); + self.nonverifiable_put_raw(key_ab, vec![]); + + // B -> A + let pair_ba = pair_ab.flip(); + let inventory_b = position + .reserves_for(pair_ba.start) + .expect("the directed trading pair is correct"); + let key_ba = eviction_queue::inventory_index::key(&pair_ba, inventory_b, id).to_vec(); + self.nonverifiable_put_raw(key_ba, vec![]); + } + + fn deindex_position_by_inventory( + &mut self, + prev_position: &position::Position, + id: &position::Id, + ) { + let canonical_pair = prev_position.phi.pair; + + // To deindex the position, we need to reconstruct the tuple of keys + // that correspond to each direction of the trading pair: + // A -> B + let pair_ab = DirectedTradingPair::new(canonical_pair.asset_1(), canonical_pair.asset_2()); + let inventory_a = prev_position + .reserves_for(pair_ab.start) + .expect("the directed trading pair is correct"); + let key_ab = eviction_queue::inventory_index::key(&pair_ab, inventory_a, id).to_vec(); + self.nonverifiable_delete(key_ab); + + // B -> A + let pair_ba = pair_ab.flip(); + let inventory_b = prev_position + .reserves_for(pair_ba.start) + .expect("the directed trading pair is correct"); + let key_ba = eviction_queue::inventory_index::key(&pair_ba, inventory_b, id).to_vec(); + self.nonverifiable_delete(key_ba); + } + fn index_position_by_price(&mut self, position: &position::Position, id: &position::Id) { let (pair, phi) = (position.phi.pair, &position.phi); if position.reserves.r2 != 0u64.into() { @@ -437,10 +490,7 @@ pub(crate) trait Inner: StateWrite { end: pair.asset_2(), }; let phi12 = phi.component.clone(); - self.nonverifiable_put_raw( - state_key::internal::price_index::key(&pair12, &phi12, &id), - vec![], - ); + self.nonverifiable_put_raw(engine::price_index::key(&pair12, &phi12, &id), vec![]); tracing::debug!("indexing position for 1=>2 trades"); } @@ -451,10 +501,7 @@ pub(crate) trait Inner: StateWrite { end: pair.asset_1(), }; let phi21 = phi.component.flip(); - self.nonverifiable_put_raw( - state_key::internal::price_index::key(&pair21, &phi21, &id), - vec![], - ); + self.nonverifiable_put_raw(engine::price_index::key(&pair21, &phi21, &id), vec![]); tracing::debug!("indexing position for 2=>1 trades"); } } @@ -471,8 +518,8 @@ pub(crate) trait Inner: StateWrite { end: position.phi.pair.asset_1(), }; let phi21 = position.phi.component.flip(); - self.nonverifiable_delete(state_key::internal::price_index::key(&pair12, &phi12, &id)); - self.nonverifiable_delete(state_key::internal::price_index::key(&pair21, &phi21, &id)); + self.nonverifiable_delete(engine::price_index::key(&pair12, &phi12, &id)); + self.nonverifiable_delete(engine::price_index::key(&pair21, &phi21, &id)); } /// Updates the nonverifiable liquidity indices given a [`Position`] in the direction specified by the [`DirectedTradingPair`]. @@ -492,7 +539,7 @@ pub(crate) trait Inner: StateWrite { // Query the current available liquidity for this trading pair, or zero if the trading pair // has no current liquidity. let current_a_from_b = self - .nonverifiable_get_raw(&state_key::internal::routable_assets::a_from_b(&pair)) + .nonverifiable_get_raw(&engine::routable_assets::a_from_b(&pair)) .await? .map(|bytes| { Amount::from_be_bytes( @@ -524,7 +571,7 @@ pub(crate) trait Inner: StateWrite { // Query the current available liquidity for this trading pair, or zero if the trading pair // has no current liquidity. let current_a_from_b = self - .nonverifiable_get_raw(&state_key::internal::routable_assets::a_from_b(&pair)) + .nonverifiable_get_raw(&engine::routable_assets::a_from_b(&pair)) .await? .map(|bytes| { Amount::from_be_bytes( @@ -562,7 +609,7 @@ pub(crate) trait Inner: StateWrite { // Query the current available liquidity for this trading pair, or zero if the trading pair // has no current liquidity. let current_a_from_b = self - .nonverifiable_get_raw(&state_key::internal::routable_assets::a_from_b(&pair)) + .nonverifiable_get_raw(&engine::routable_assets::a_from_b(&pair)) .await? .map(|bytes| { Amount::from_be_bytes( @@ -597,20 +644,20 @@ pub(crate) trait Inner: StateWrite { // Delete the existing key for this position if the reserve amount has changed. if new_a_from_b != current_a_from_b { self.nonverifiable_delete( - state_key::internal::routable_assets::key(&pair.start, current_a_from_b).to_vec(), + engine::routable_assets::key(&pair.start, current_a_from_b).to_vec(), ); } // Write the new key indicating that asset B is routable from asset A with `new_a_from_b` liquidity. self.nonverifiable_put_raw( - state_key::internal::routable_assets::key(&pair.start, new_a_from_b).to_vec(), + engine::routable_assets::key(&pair.start, new_a_from_b).to_vec(), pair.end.encode_to_vec(), ); tracing::debug!(start = ?pair.start, end = ?pair.end, "marking routable from start -> end"); // Write the new lookup index storing `new_a_from_b` for this trading pair. self.nonverifiable_put_raw( - state_key::internal::routable_assets::a_from_b(&pair).to_vec(), + engine::routable_assets::a_from_b(&pair).to_vec(), new_a_from_b.to_be_bytes().to_vec(), ); tracing::debug!(available_liquidity = ?new_a_from_b, ?pair, "marking available liquidity for trading pair"); diff --git a/crates/core/component/dex/src/state_key.rs b/crates/core/component/dex/src/state_key.rs index 05e3e85ee3..4b2646c3dd 100644 --- a/crates/core/component/dex/src/state_key.rs +++ b/crates/core/component/dex/src/state_key.rs @@ -79,20 +79,19 @@ pub fn aggregate_value() -> &'static str { "dex/aggregate_value" } -/// Encompasses non-consensus state keys. -pub(crate) mod internal { +pub(crate) mod engine { use super::*; use crate::lp::BareTradingFunction; - pub mod counter { - pub mod num_positions { + pub(crate) mod counter { + pub(crate) mod num_positions { use crate::TradingPair; - pub fn prefix() -> &'static str { + pub(crate) fn prefix() -> &'static str { "dex/internal/counter/num_positions/" } - pub fn by_trading_pair(trading_pair: &TradingPair) -> [u8; 99] { + pub(crate) fn by_trading_pair(trading_pair: &TradingPair) -> [u8; 99] { let mut key = [0u8; 99]; let prefix_bytes = prefix().as_bytes(); let canonical_pair_bytes = trading_pair.to_bytes(); @@ -103,8 +102,9 @@ pub(crate) mod internal { } } } + /// Find assets with liquidity positions from asset `from`, ordered by price. - pub mod routable_assets { + pub(crate) mod routable_assets { use penumbra_asset::asset; use penumbra_num::Amount; @@ -113,7 +113,7 @@ pub(crate) mod internal { /// `A || be_bytes(A_from_B) => B` this will be an ordered encoding of every asset `B` directly routable to from `A`. /// `a_from_b` represents the amount of `A` that can be bought with `B`. /// The prefix query includes only the `A` portion, meaning the keys will be returned in order of liquidity. - pub fn prefix(from: &asset::Id) -> [u8; 39] { + pub(crate) fn prefix(from: &asset::Id) -> [u8; 39] { let mut key = [0u8; 39]; key[0..7].copy_from_slice(b"dex/ra/"); key[7..7 + 32].copy_from_slice(&from.to_bytes()); @@ -122,7 +122,7 @@ pub(crate) mod internal { /// `A || be_bytes(A_from_B) => B` this will be an ordered encoding of every asset `B` directly routable to from `A`. /// `a_from_b` represents the amount of `A` that can be bought with `B`. - pub fn key(from: &asset::Id, a_from_b: Amount) -> [u8; 55] { + pub(crate) fn key(from: &asset::Id, a_from_b: Amount) -> [u8; 55] { let mut key = [0u8; 55]; key[0..7].copy_from_slice(b"dex/ra/"); key[7..32 + 7].copy_from_slice(&from.to_bytes()); @@ -132,7 +132,7 @@ pub(crate) mod internal { /// `(A, B) => A_from_B` this will encode the current amount of `A` tradable into `B` for every directly routable trading pair. /// This index can be used to determine the key values for the [`super::key`] ordered index to perform updates efficiently. - pub fn a_from_b(pair: &DirectedTradingPair) -> [u8; 71] { + pub(crate) fn a_from_b(pair: &DirectedTradingPair) -> [u8; 71] { let mut key = [0u8; 71]; key[0..7].copy_from_slice(b"dex/ab/"); key[7..7 + 32].copy_from_slice(&pair.start.to_bytes()); @@ -141,10 +141,10 @@ pub(crate) mod internal { } } - pub mod price_index { + pub(crate) mod price_index { use super::*; - pub fn prefix(pair: &DirectedTradingPair) -> [u8; 71] { + pub(crate) fn prefix(pair: &DirectedTradingPair) -> [u8; 71] { let mut key = [0u8; 71]; key[0..7].copy_from_slice(b"dex/pi/"); key[7..7 + 32].copy_from_slice(&pair.start.to_bytes()); @@ -152,7 +152,7 @@ pub(crate) mod internal { key } - pub fn key( + pub(crate) fn key( pair: &DirectedTradingPair, btf: &BareTradingFunction, id: &position::Id, @@ -166,3 +166,33 @@ pub(crate) mod internal { } } } + +pub(crate) mod eviction_queue { + pub(crate) mod inventory_index { + use crate::lp::position; + use crate::DirectedTradingPair; + use penumbra_num::Amount; + + pub(crate) fn by_trading_pair(pair: &DirectedTradingPair) -> [u8; 107] { + let mut prefix = [0u8; 107]; + prefix[0..43].copy_from_slice(b"dex/internal/eviction_queue/inventory_index"); + prefix[43..75].copy_from_slice(&pair.start.to_bytes()); + prefix[75..107].copy_from_slice(&pair.end.to_bytes()); + prefix + } + + pub(crate) fn key( + pair: &DirectedTradingPair, + inventory: Amount, + id: &position::Id, + ) -> [u8; 155] { + let mut full_key = [0u8; 155]; + let prefix = by_trading_pair(pair); + full_key[0..107].copy_from_slice(&prefix); + full_key[107..123].copy_from_slice(&inventory.to_be_bytes()); + full_key[123..155].copy_from_slice(&id.0); + + full_key + } + } +} From 15e5e06dbc783d857d7c3ec6c05d426dcb3b0058 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Tue, 9 Apr 2024 16:34:54 -0400 Subject: [PATCH 085/168] =?UTF-8?q?docs(mock-consensus):=20=F0=9F=A6=85=20?= =?UTF-8?q?document=20various=20testing=20interfaces=20(#4184)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes #4181. see #3588. this makes a pass through the `penumbra-mock-consensus` library and further documents various interfaces. one other small tweak, logging a warning if a caller may be inadvertently discarding transactions, is made while we are here. these docs may be rendered by running: `cargo doc --package penumbra-mock-consensus --open` --------- Co-authored-by: Conor Schaefer --- crates/test/mock-consensus/src/block.rs | 33 ++++++-- crates/test/mock-consensus/src/builder.rs | 2 +- .../mock-consensus/src/builder/init_chain.rs | 5 ++ crates/test/mock-consensus/src/lib.rs | 83 ++++++++++++++++--- deployments/scripts/rust-docs | 1 + 5 files changed, 101 insertions(+), 23 deletions(-) diff --git a/crates/test/mock-consensus/src/block.rs b/crates/test/mock-consensus/src/block.rs index 34a1e02bc8..6b35953b20 100644 --- a/crates/test/mock-consensus/src/block.rs +++ b/crates/test/mock-consensus/src/block.rs @@ -1,9 +1,6 @@ //! [`Builder`] facilities for constructing [`Block`]s. //! -//! Builders are acquired by calling [`TestNode::block()`]. - -/// Interfaces for generating commit signatures. -mod signature; +//! Builders are acquired by calling [`TestNode::block()`], see [`TestNode`] for more information. use { crate::TestNode, @@ -19,19 +16,24 @@ use { tracing::{instrument, trace}, }; -/// A builder, used to prepare and instantiate a new [`Block`]. +/// Interfaces for generating commit signatures. +mod signature; + +/// A block builder. +/// +/// A block builder can be used to prepare and instantiate a new [`Block`]. A block builder is +/// acquired by calling [`TestNode::block()`]. This builder holds an exclusive reference to a +/// [`TestNode`], so only one block may be built at once. /// -/// These are acquired by calling [`TestNode::block()`]. +/// This builder can be consumed, executing the block against the [`TestNode`]'s consensus service, +/// by calling [`Builder::execute()`]. pub struct Builder<'e, C> { /// A unique reference to the test node. test_node: &'e mut TestNode, - /// Transaction data. data: Vec>, - /// Evidence of malfeasance. evidence: evidence::List, - /// The list of signatures. signatures: Vec, } @@ -40,6 +42,10 @@ pub struct Builder<'e, C> { impl TestNode { /// Returns a new [`Builder`]. + /// + /// By default, signatures for all of the validators currently within the keyring will be + /// included in the block. Use [`Builder::with_signatures()`] to set a different set of + /// validator signatures. pub fn block<'e>(&'e mut self) -> Builder<'e, C> { let signatures = self.generate_signatures().collect(); Builder { @@ -56,6 +62,15 @@ impl TestNode { impl<'e, C> Builder<'e, C> { /// Sets the data for this block. pub fn with_data(self, data: Vec>) -> Self { + let Self { data: prev, .. } = self; + + if !prev.is_empty() { + tracing::warn!( + count = %prev.len(), + "block builder overwriting transaction data, this may be a bug!" + ); + } + Self { data, ..self } } diff --git a/crates/test/mock-consensus/src/builder.rs b/crates/test/mock-consensus/src/builder.rs index 80c86ca21b..f27573b225 100644 --- a/crates/test/mock-consensus/src/builder.rs +++ b/crates/test/mock-consensus/src/builder.rs @@ -10,7 +10,7 @@ use { bytes::Bytes, }; -/// A buider, used to prepare and instantiate a new [`TestNode`]. +/// A builder, used to prepare and instantiate a new [`TestNode`]. #[derive(Default)] pub struct Builder { pub app_state: Option, diff --git a/crates/test/mock-consensus/src/builder/init_chain.rs b/crates/test/mock-consensus/src/builder/init_chain.rs index bd2f8c95d2..a45325d0b4 100644 --- a/crates/test/mock-consensus/src/builder/init_chain.rs +++ b/crates/test/mock-consensus/src/builder/init_chain.rs @@ -19,6 +19,11 @@ use { impl Builder { /// Consumes this builder, using the provided consensus service. + /// + /// This function returns an error if the builder was not fully initialized, or if the + /// application could not successfully perform the chain initialization. + /// + /// See [`TestNode`] for more information on the consensus service. pub async fn init_chain(self, mut consensus: C) -> Result, anyhow::Error> where C: Service diff --git a/crates/test/mock-consensus/src/lib.rs b/crates/test/mock-consensus/src/lib.rs index 13ae230924..0127150dee 100644 --- a/crates/test/mock-consensus/src/lib.rs +++ b/crates/test/mock-consensus/src/lib.rs @@ -1,8 +1,33 @@ -//! `penumbra-mock-consensus` is a library for testing consensus-driven applications. +//! `penumbra-mock-consensus` is a library for testing consensus-driven ABCI applications. //! -//! See [`TestNode`] for more information. -// -// see penumbra-zone/penumbra#3588. +//! # Overview +//! +//! This library provides facilities that can act as a stand-in for consensus engines like +//! [CometBFT][cometbft] or [Tendermint][tendermint] in integration tests. +//! +//! Testing applications using a mock consensus engine has many benefits. For example, this allows +//! integration test cases to run as fast as possible, without needing wait real wall-clock time +//! for blocks to be generated, or for integration test cases to exercise slashing logic related to +//! byzantine misbehavior (_e.g., double-signing_). +//! +//! This library is agnostic with respect to the replicable state transition machine that it +//! is used to test. This means that, while it may be used to write integration tests for the +//! [Penumbra][penumbra] network, it can also be used to test other decentralized applications. +//! +//! See [`TestNode`] for more information about using `penumbra-mock-consensus`. +//! +//! # Alternatives +//! +//! Projects implemented in Go may wish to consider using [CometMock][cometmock]. +//! `penumbra-mock-consensus` is primarily oriented towards projects implemented in Rust that wish +//! to use [`cargo test`][cargo-test] or [`cargo nextest`][cargo-nextest] as a test-runner. +//! +//! [cargo-nextest]: https://nexte.st/ +//! [cargo-test]: https://doc.rust-lang.org/cargo/commands/cargo-test.html +//! [cometbft]: https://github.com/cometbft/cometbft +//! [cometmock]: https://github.com/informalsystems/CometMock +//! [penumbra]: https://github.com/penumbra-zone/penumbra +//! [tendermint]: https://github.com/tendermint/tendermint use { ed25519_consensus::{SigningKey, VerificationKey}, @@ -16,17 +41,46 @@ mod abci; /// A test node. /// -/// Construct a new test node by calling [`TestNode::builder()`]. Use [`TestNode::block()`] to -/// build a new [`Block`]. +/// A [`TestNode`] represents a validator node containing an instance of the state transition +/// machine and its accompanying consensus engine. +/// +/// # Initialization +/// +/// Construct a new test node by calling [`TestNode::builder()`]. The [`builder::Builder`] +/// returned by that method can be used to set the initial application state, and configure +/// validators' consensus keys that should be present at genesis. Use +/// [`builder::Builder::init_chain()`] to consume the builder and initialize the application. +/// +/// # Consensus Service +/// +/// A test node is generic in terms of a consensus service `C`. This service should implement +/// [`tower::Service`], accepting [`ConsensusRequest`][consensus-request]s, and returning +/// [`ConsensusResponse`][consensus-response]s. +/// +/// For [`tower-abci`][tower-abci] users, this should correspond with the `C` parameter of the +/// [`Server`][tower-abci-server] type. +/// +/// # Blocks +/// +/// Blocks can be executed by using [`TestNode::block()`]. This can be used to add transactions, +/// signatures, and evidence to a [`Block`][tendermint-rs-block], before invoking +/// [`block::Builder::execute()`] to execute the next block. /// -/// This contains a consensus service `C`, which should be a [`tower::Service`] implementor that -/// accepts [`ConsensusRequest`][0_37::abci::ConsensusRequest]s, and returns -/// [`ConsensusResponse`][0_37::abci::ConsensusResponse]s. For `tower-abci` users, this should -/// correspond with the `ConsensusService` parameter of the `Server` type. +/// [consensus-request]: tendermint::v0_37::abci::ConsensusRequest +/// [consensus-response]: tendermint::v0_37::abci::ConsensusResponse +/// [tendermint-rs-block]: tendermint::block::Block +/// [tower-abci-server]: https://docs.rs/tower-abci/latest/tower_abci/v037/struct.Server.html# +/// [tower-abci]: https://docs.rs/tower-abci/latest/tower_abci pub struct TestNode { + /// The inner consensus service being tested. consensus: C, + /// The last `app_hash` value. last_app_hash: Vec, + /// The current block [`Height`][tendermint::block::Height]. height: tendermint::block::Height, + /// Validators' consensus keys. + /// + /// Entries in this keyring consist of a [`VerificationKey`] and a [`SigningKey`]. keyring: Keyring, } @@ -35,15 +89,17 @@ pub struct TestNode { /// Entries in this keyring consist of a [`VerificationKey`] and a [`SigningKey`]. type Keyring = BTreeMap; +/// Accessors. impl TestNode { + /// A chain ID for use in tests. pub const CHAIN_ID: &'static str = "penumbra-test-chain"; - /// Returns the last app_hash value, as a slice of bytes. + /// Returns the last `app_hash` value, represented as a slice of bytes. pub fn last_app_hash(&self) -> &[u8] { &self.last_app_hash } - /// Returns the last app_hash value, as a hexadecimal string. + /// Returns the last `app_hash` value, represented as a hexadecimal string. pub fn last_app_hash_hex(&self) -> String { // Use upper-case hexadecimal integers, include leading zeroes. // - https://doc.rust-lang.org/std/fmt/#formatting-traits @@ -61,6 +117,7 @@ impl TestNode { } } +/// Fast forward interfaces. impl TestNode where C: tower::Service< @@ -73,7 +130,7 @@ where C::Future: Send + 'static, C::Error: Sized, { - /// Fast forwards a number of blocks. + /// Fast forwards the given number of blocks. #[tracing::instrument( skip(self), fields(fast_forward.blocks = %blocks) diff --git a/deployments/scripts/rust-docs b/deployments/scripts/rust-docs index 1eaad2ebc5..2ffa5d4768 100755 --- a/deployments/scripts/rust-docs +++ b/deployments/scripts/rust-docs @@ -42,6 +42,7 @@ cargo +nightly doc --no-deps \ -p penumbra-ibc \ -p penumbra-keys \ -p penumbra-measure \ + -p penumbra-mock-consensus \ -p penumbra-num \ -p penumbra-proof-params \ -p penumbra-proof-setup \ From dc4336bb6c3f1f2c5cadca652b123bc87cd8a2d2 Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Fri, 5 Apr 2024 14:22:57 -0400 Subject: [PATCH 086/168] spec: fill in missing TODOs in balance commitment by action table --- docs/protocol/src/transactions/actions.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/protocol/src/transactions/actions.md b/docs/protocol/src/transactions/actions.md index 05b9fa4462..039ec46699 100644 --- a/docs/protocol/src/transactions/actions.md +++ b/docs/protocol/src/transactions/actions.md @@ -39,11 +39,11 @@ contributions together. | `dex.v1.PositionClose` | Closes a liquidity position | | $-$ (opened LPNFT) $+$ (closed LPNFT) | | `dex.v1.PositionWithdraw` | Withdraws reserves or rewards from a liquidity position, with sequence number $n$ | | $-$ (withdrawn seq $n-1$ LPNFT) $+$ (withdrawn seq $n$ LPNFT) $+$ (current position reserves) | | `dex.v1.PositionRewardClaim` | Deprecated and unused | | | -| `governance.v1.ProposalSubmit` | Submits a governance proposal for voting | TODO | TODO | -| `governance.v1.ProposalWithdraw` | Withdraws a governance proposal from voting | TODO | TODO | -| `governance.v1.ValidatorVote` | Performs a governance vote as a validator | TODO | TODO | -| [`governance.v1.DelegatorVote`](../governance/action/delegator_vote.md) | Performs a governance vote as a delegator | TODO | TODO | -| `governance.v1.ProposalDepositClaim` | Claims a proposal deposit once voting has finished | TODO | TODO | +| `governance.v1.ProposalSubmit` | Submits a governance proposal for voting | | $-$ (deposit amount) $+$ (voting proposal NFT) | +| `governance.v1.ProposalWithdraw` | Withdraws a governance proposal from voting | | $-$ (voting proposal NFT) $+$ (withdrawn proposal NFT) | +| `governance.v1.ValidatorVote` | Performs a governance vote as a validator | | | +| [`governance.v1.DelegatorVote`](../governance/action/delegator_vote.md) | Performs a governance vote as a delegator | | $+$ (Voting Receipt Token) | +| `governance.v1.ProposalDepositClaim` | Claims a proposal deposit once voting has finished | | $-$ (voting/withdrawn proposal NFT) $+$ (claimed proposal NFT) $+$ (deposit amount, if not slashed) | | `governance.v1.CommunityPoolSpend` | Spends funds from the community pool | | $+$ (spent value) | | `governance.v1.CommunityPoolOutput` | Like `Output`, but transparent | | $-$ (value of new note) | `governance.v1.CommunityPoolDeposit` | Allows deposits into the community pool | | $-$ (value of deposit) | \ No newline at end of file From 9d64ddae4c41c12413c139dbe05efefb8624e1f1 Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Fri, 5 Apr 2024 14:31:18 -0400 Subject: [PATCH 087/168] replace usage of value commitment term with balance commitment this is in all non-proto files, i.e. code comments and spec --- crates/core/asset/src/asset/id.rs | 2 +- crates/core/asset/src/balance.rs | 2 +- crates/core/asset/src/value.rs | 2 +- crates/core/component/dex/src/lp/nft.rs | 2 +- .../core/component/governance/src/delegator_vote/proof.rs | 4 ++-- crates/core/component/shielded-pool/src/output/action.rs | 2 +- crates/core/component/shielded-pool/src/output/proof.rs | 2 +- crates/core/component/shielded-pool/src/spend/action.rs | 2 +- crates/core/component/shielded-pool/src/spend/proof.rs | 4 ++-- crates/core/keys/src/symmetric.rs | 2 +- docs/protocol/src/addresses_keys/transaction_crypto.md | 2 +- docs/protocol/src/assets.md | 6 +++--- .../src/crypto/flow-encryption/threshold-encryption.md | 2 +- docs/protocol/src/dex/action/swap.md | 2 +- docs/protocol/src/transactions.md | 6 +++--- 15 files changed, 21 insertions(+), 21 deletions(-) diff --git a/crates/core/asset/src/asset/id.rs b/crates/core/asset/src/asset/id.rs index a78cf0bba8..c3d4ba186f 100644 --- a/crates/core/asset/src/asset/id.rs +++ b/crates/core/asset/src/asset/id.rs @@ -132,7 +132,7 @@ pub static VALUE_GENERATOR_DOMAIN_SEP: Lazy = Lazy::new(|| { }); impl Id { - /// Compute the value commitment generator for this asset. + /// Compute the value generator for this asset, used for computing balance commitments. pub fn value_generator(&self) -> decaf377::Element { decaf377::Element::encode_to_curve(&poseidon377::hash_1( &VALUE_GENERATOR_DOMAIN_SEP, diff --git a/crates/core/asset/src/balance.rs b/crates/core/asset/src/balance.rs index 4201a853fa..d41be59b55 100644 --- a/crates/core/asset/src/balance.rs +++ b/crates/core/asset/src/balance.rs @@ -537,7 +537,7 @@ mod test { proptest! { /// Checks to make sure that any possible expression made of negation, addition, and - /// subtraction is a homomorphism with regard to the resultant value commitment, which + /// subtraction is a homomorphism with regard to the resultant balance commitment, which /// should provide assurance that these operations are implemented correctly on the balance /// type itself. #[test] diff --git a/crates/core/asset/src/value.rs b/crates/core/asset/src/value.rs index 63de88a024..23b1ae49ef 100644 --- a/crates/core/asset/src/value.rs +++ b/crates/core/asset/src/value.rs @@ -189,7 +189,7 @@ impl TryFrom for Value { .try_into()?, asset_id: value .asset_id - .ok_or_else(|| anyhow::anyhow!("missing value commitment"))? + .ok_or_else(|| anyhow::anyhow!("missing balance commitment"))? .try_into()?, }) } diff --git a/crates/core/component/dex/src/lp/nft.rs b/crates/core/component/dex/src/lp/nft.rs index bee25ac208..fe4174b1d6 100644 --- a/crates/core/component/dex/src/lp/nft.rs +++ b/crates/core/component/dex/src/lp/nft.rs @@ -17,7 +17,7 @@ use super::position::{Id, State}; /// be recorded in a programmatic on-chain account (in the future, e.g., to /// support interchain accounts). This also means that LP-related actions don't /// require any cryptographic implementation (proofs, signatures, etc), other -/// than hooking into the value commitment mechanism used for transaction +/// than hooking into the balance commitment mechanism used for transaction /// balances. #[derive(Debug, Clone)] pub struct LpNft { diff --git a/crates/core/component/governance/src/delegator_vote/proof.rs b/crates/core/component/governance/src/delegator_vote/proof.rs index aa1ae6f0fa..201afd1ed2 100644 --- a/crates/core/component/governance/src/delegator_vote/proof.rs +++ b/crates/core/component/governance/src/delegator_vote/proof.rs @@ -38,7 +38,7 @@ use tap::Tap; pub struct DelegatorVoteProofPublic { /// the merkle root of the state commitment tree. pub anchor: tct::Root, - /// value commitment of the note to be spent. + /// balance commitment of the note to be spent. pub balance_commitment: balance::Commitment, /// nullifier of the note to be spent. pub nullifier: Nullifier, @@ -55,7 +55,7 @@ pub struct DelegatorVoteProofPrivate { pub state_commitment_proof: tct::Proof, /// The note being spent. pub note: Note, - /// The blinding factor used for generating the value commitment. + /// The blinding factor used for generating the balance commitment. pub v_blinding: Fr, /// The randomizer used for generating the randomized spend auth key. pub spend_auth_randomizer: Fr, diff --git a/crates/core/component/shielded-pool/src/output/action.rs b/crates/core/component/shielded-pool/src/output/action.rs index 1bb5e4b0c3..b98d0026a9 100644 --- a/crates/core/component/shielded-pool/src/output/action.rs +++ b/crates/core/component/shielded-pool/src/output/action.rs @@ -108,7 +108,7 @@ impl TryFrom for Body { let balance_commitment = proto .balance_commitment - .ok_or_else(|| anyhow::anyhow!("missing value commitment"))? + .ok_or_else(|| anyhow::anyhow!("missing balance commitment"))? .try_into() .context("malformed balance commitment")?; diff --git a/crates/core/component/shielded-pool/src/output/proof.rs b/crates/core/component/shielded-pool/src/output/proof.rs index abbcfb0966..1cae074257 100644 --- a/crates/core/component/shielded-pool/src/output/proof.rs +++ b/crates/core/component/shielded-pool/src/output/proof.rs @@ -89,7 +89,7 @@ fn check_circuit_satisfaction( } /// Public: -/// * vcm (value commitment) +/// * vcm (balance commitment) /// * ncm (note commitment) /// /// Witnesses: diff --git a/crates/core/component/shielded-pool/src/spend/action.rs b/crates/core/component/shielded-pool/src/spend/action.rs index edb2d96ee8..2a548a592a 100644 --- a/crates/core/component/shielded-pool/src/spend/action.rs +++ b/crates/core/component/shielded-pool/src/spend/action.rs @@ -101,7 +101,7 @@ impl TryFrom for Body { fn try_from(proto: pb::SpendBody) -> anyhow::Result { let balance_commitment: balance::Commitment = proto .balance_commitment - .ok_or_else(|| anyhow::anyhow!("missing value commitment"))? + .ok_or_else(|| anyhow::anyhow!("missing balance commitment"))? .try_into() .context("malformed balance commitment")?; diff --git a/crates/core/component/shielded-pool/src/spend/proof.rs b/crates/core/component/shielded-pool/src/spend/proof.rs index b46e262e0f..a818fc8267 100644 --- a/crates/core/component/shielded-pool/src/spend/proof.rs +++ b/crates/core/component/shielded-pool/src/spend/proof.rs @@ -43,7 +43,7 @@ use tap::Tap; pub struct SpendProofPublic { /// the merkle root of the state commitment tree. pub anchor: tct::Root, - /// value commitment of the note to be spent. + /// balance commitment of the note to be spent. pub balance_commitment: balance::Commitment, /// nullifier of the note to be spent. pub nullifier: Nullifier, @@ -58,7 +58,7 @@ pub struct SpendProofPrivate { pub state_commitment_proof: tct::Proof, /// The note being spent. pub note: Note, - /// The blinding factor used for generating the value commitment. + /// The blinding factor used for generating the balance commitment. pub v_blinding: Fr, /// The randomizer used for generating the randomized spend auth key. pub spend_auth_randomizer: Fr, diff --git a/crates/core/keys/src/symmetric.rs b/crates/core/keys/src/symmetric.rs index ff4d608f92..d3f1aa8536 100644 --- a/crates/core/keys/src/symmetric.rs +++ b/crates/core/keys/src/symmetric.rs @@ -222,7 +222,7 @@ impl OutgoingCipherKey { // Note: Here we use the same nonce as note encryption, however the keys are different. // For note encryption we derive the `PayloadKey` symmetric key from the shared secret and epk. // However, for the outgoing cipher key, we derive a symmetric key from the - // sender's OVK, value commitment, note commitment, and the epk. Since the keys are + // sender's OVK, balance commitment, note commitment, and the epk. Since the keys are // different, it is safe to use the same nonce. // // References: diff --git a/docs/protocol/src/addresses_keys/transaction_crypto.md b/docs/protocol/src/addresses_keys/transaction_crypto.md index 5d37688e38..c2b9040100 100644 --- a/docs/protocol/src/addresses_keys/transaction_crypto.md +++ b/docs/protocol/src/addresses_keys/transaction_crypto.md @@ -84,7 +84,7 @@ should be able to decrypt the per-transaction memo. ### Outgoing Cipher Key The symmetric outgoing cipher key is a 32-byte key derived from the sender's outgoing viewing key -$ovk$, the value commitment $cv$, the note commitment $cm$, the ephemeral +$ovk$, the balance commitment $cv$, the note commitment $cm$, the ephemeral public key $epk$, and personalization string "Penumbra_OutCiph": ``` diff --git a/docs/protocol/src/assets.md b/docs/protocol/src/assets.md index 0a3bb85f12..1f39c7191d 100644 --- a/docs/protocol/src/assets.md +++ b/docs/protocol/src/assets.md @@ -8,7 +8,7 @@ context. ## Asset types and asset IDs -To be precise, we define: +To be precise, we define: - an *amount* to be an untyped `u128` quantity of some asset; - an *asset ID* to be an $\mathbb F_q$ element; @@ -58,7 +58,7 @@ rate-1 Poseidon hashing with domain separator `from_le_bytes(b"penumbra.value.generator")` and then the `decaf377` CDH map-to-group method. -## Homomorphic Value Commitments +## Homomorphic Balance Commitments We use the value generator associated to an asset ID to construct homomorphic commitments to (typed) value. To do this, we first define the *blinding @@ -77,7 +77,7 @@ These commitments are homomorphic, even for different asset types, say values $(x, \mathsf a)$ and $(y, \mathsf b)$: $$ ([x]V_{\mathsf a} + [\widetilde{x}]\widetilde{V}) + ([y] V_{\mathsf b} + [\widetilde{y}]\widetilde{V}) -= += [x]V_{\mathsf a} + [y] V_{\mathsf b} + [\widetilde{x} + \widetilde{y}]\widetilde{V}. $$ Alternatively, this can be thought of as a commitment to a (sparse) vector diff --git a/docs/protocol/src/crypto/flow-encryption/threshold-encryption.md b/docs/protocol/src/crypto/flow-encryption/threshold-encryption.md index e39cbed3d9..0f4e0d53bd 100644 --- a/docs/protocol/src/crypto/flow-encryption/threshold-encryption.md +++ b/docs/protocol/src/crypto/flow-encryption/threshold-encryption.md @@ -11,7 +11,7 @@ scheme described here. For our threshold encryption scheme, we require three important properties: -* Homomorphism: we must be able to operate over ciphertexts, by combining value commitments from many participants into a batched value. +* Homomorphism: we must be able to operate over ciphertexts, by combining balance commitments from many participants into a batched value. * Verifiability: we must be able to verify that a given value $v_i$ was encrypted correctly to a given ciphertext $c_i$ * Robustness: up to $n-t$ validators must be permitted to either fail to provide a decryption share or provide in invalid decryption share. diff --git a/docs/protocol/src/dex/action/swap.md b/docs/protocol/src/dex/action/swap.md index cf3143cc04..8095c1023f 100644 --- a/docs/protocol/src/dex/action/swap.md +++ b/docs/protocol/src/dex/action/swap.md @@ -123,4 +123,4 @@ The zk-SNARK certifies that the total public input balance commitment $cv$ was d $cv = [-v_1] G_1 + [-v_2] G_2 + cv_f$ -where the first two terms are from the input amounts and assets, with the corresponding asset-specific generators $G_1, G_2$ derived in-circuit as described in [Value Commitments](../../assets.md), and $cv_f$ is the fee commitment. +where the first two terms are from the input amounts and assets, with the corresponding asset-specific generators $G_1, G_2$ derived in-circuit as described in [Balance Commitments](../../assets.md), and $cv_f$ is the fee commitment. diff --git a/docs/protocol/src/transactions.md b/docs/protocol/src/transactions.md index 2f5cd26c37..baa0e4171b 100644 --- a/docs/protocol/src/transactions.md +++ b/docs/protocol/src/transactions.md @@ -28,17 +28,17 @@ The chain requires that transactions do not create or destroy value. To accomplish conservation of value, the _binding signature_ proves that the transaction's value balance, summed up over all actions, is zero. This construction works as follows. We'd like to be able to prove that a certain -value commitment $C$ is a commitment to $0$. One way to do this would be to +balance commitment $C$ is a commitment to $0$. One way to do this would be to prove knowledge of an opening to the commitment, i.e., producing $\widetilde{v}$ such that $$C = [\widetilde{v}] \widetilde{V} = \operatorname{Commit}(0, \widetilde{v}).$$ But this is exactly what it means to create a Schnorr signature for the verification key $C$, because a Schnorr signature is a proof of knowledge of the signing key in the context of the message. -Therefore, we can prove that a value commitment is a commitment to $0$ by +Therefore, we can prove that a balance commitment is a commitment to $0$ by treating it as a `decaf377-rdsa` verification key and using the corresponding signing key (the blinding factor) to sign a message. This also gives a way to -bind value commitments to a particular context (e.g., a transaction), by using a +bind balance commitments to a particular context (e.g., a transaction), by using a hash of the transaction as the message to be signed, ensuring that actions cannot be replayed across transactions without knowledge of their contents. From d8fcc9d3e274295cc55ad821b88ce2c3a316c455 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Wed, 10 Apr 2024 22:48:43 -0400 Subject: [PATCH 088/168] =?UTF-8?q?tests(app):=20=F0=9F=99=85=20app=20reje?= =?UTF-8?q?cts=20invalid=20auth=20signatures=20(#4182)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes #4050. see also #3588. this introduces a test case that demonstrates that transactions' validator definition actions must have a valid authentication signature, or the validator will not be added to the set of known validators. - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > this is a test case. --- ...ator_definitions_with_invalid_auth_sigs.rs | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 crates/core/app/tests/app_rejects_validator_definitions_with_invalid_auth_sigs.rs diff --git a/crates/core/app/tests/app_rejects_validator_definitions_with_invalid_auth_sigs.rs b/crates/core/app/tests/app_rejects_validator_definitions_with_invalid_auth_sigs.rs new file mode 100644 index 0000000000..679a4048cb --- /dev/null +++ b/crates/core/app/tests/app_rejects_validator_definitions_with_invalid_auth_sigs.rs @@ -0,0 +1,142 @@ +use { + self::common::BuilderExt, + cnidarium::TempStorage, + decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey}, + penumbra_app::{genesis::AppState, server::consensus::Consensus}, + penumbra_keys::test_keys, + penumbra_mock_client::MockClient, + penumbra_mock_consensus::TestNode, + penumbra_proto::DomainType, + penumbra_stake::{ + component::validator_handler::ValidatorDataRead as _, validator::Validator, FundingStreams, + GovernanceKey, IdentityKey, + }, + rand_core::OsRng, + tap::Tap, + tracing::{error_span, info, Instrument}, +}; + +mod common; + +/// Show that the application rejects validator definitions with an invalid auth signature. +#[tokio::test] +async fn app_rejects_validator_definitions_with_invalid_auth_sigs() -> anyhow::Result<()> { + // Install a test logger, and acquire some temporary storage. + let guard = common::set_tracing_subscriber(); + let storage = TempStorage::new().await?; + + // Start the test node. + let mut node = { + let consensus = Consensus::new(storage.as_ref().clone()); + let app_state = AppState::default(); + TestNode::builder() + .single_validator() + .with_penumbra_auto_app_state(app_state)? + .init_chain(consensus) + .await + }?; + + // Assert that there is only a single validator definition present, before we go any further. + assert_eq!( + storage + .latest_snapshot() + .validator_definitions() + .await? + .len(), + 1, + "invalid validator definition transactions should not be accepted" + ); + + // Sync the mock client, using the test wallet's spend key, to the latest snapshot. + let client = MockClient::new(test_keys::SPEND_KEY.clone()) + .with_sync_to_storage(&storage) + .await? + .tap(|c| info!(client.notes = %c.notes.len(), "mock client synced to test storage")); + + // To define a validator, we need to define two keypairs: an identity key + // for the Penumbra application and a consensus key for cometbft. + let new_validator_id_sk = SigningKey::::new(OsRng); + let new_validator_id = IdentityKey(VerificationKey::from(&new_validator_id_sk).into()); + let new_validator_consensus_sk = ed25519_consensus::SigningKey::new(OsRng); + let new_validator_consensus = new_validator_consensus_sk.verification_key(); + + // Create a different signing key, which we will use to create a forged authentication + // signature in our validator definition transaction. + let different_signing_key = SigningKey::::new(OsRng); + + // Insert the validator's consensus keypair into the keyring so it can be used to sign blocks. + node.keyring_mut() + // Keyring should just be a BTreeMap rather than creating a new API + .insert(new_validator_consensus, new_validator_consensus_sk); + + // Now define the validator's configuration data. + let new_validator = Validator { + identity_key: new_validator_id.clone(), + // TODO: when https://github.com/informalsystems/tendermint-rs/pull/1401 is released, + // replace this with a direct `Into::into()` call. at the time of writing, v0.35.0 is the + // latest version. check for new releases at https://crates.io/crates/tendermint/versions. + consensus_key: tendermint::PublicKey::from_raw_ed25519(&new_validator_consensus.to_bytes()) + .expect("consensus key is valid"), + governance_key: GovernanceKey(new_validator_id_sk.into()), + enabled: true, + sequence_number: 0, + name: "test validator".to_string(), + website: String::default(), + description: String::default(), + funding_streams: FundingStreams::default(), + }; + + // Make a transaction that defines a new validator, providing an invalid signature. + let plan = { + use { + penumbra_stake::validator, + penumbra_transaction::{ActionPlan, TransactionParameters, TransactionPlan}, + rand_core::OsRng, + }; + let bytes = new_validator.encode_to_vec(); + // NB: we do NOT use the validator's signing key here. this transaction will contain an + // invalid authentication signature. + let auth_sig = different_signing_key.sign(OsRng, &bytes); + let action = ActionPlan::ValidatorDefinition(validator::Definition { + validator: new_validator.clone(), + auth_sig, + }); + let mut plan = TransactionPlan { + actions: vec![action.into()], + // Now fill out the remaining parts of the transaction needed for verification: + memo: None, + detection_data: None, // We'll set this automatically below + transaction_parameters: TransactionParameters { + chain_id: TestNode::<()>::CHAIN_ID.to_string(), + ..Default::default() + }, + }; + plan.populate_detection_data(rand_core::OsRng, 0); + plan + }; + let tx = client.witness_auth_build(&plan).await?; + + // Execute the transaction, applying it to the chain state. + node.block() + .add_tx(tx.encode_to_vec()) + .execute() + .instrument(error_span!( + "executing block with validator definition transaction" + )) + .await?; + let post_tx_snapshot = storage.latest_snapshot(); + + // Assert that there is still only a single validator definition present, and that this + // invalid transaction did not cause a new definition to appear. + assert_eq!( + post_tx_snapshot.validator_definitions().await?.len(), + 1, + "invalid validator definition transactions should not be accepted" + ); + + // The test passed. Free our temporary storage and drop our tracing subscriber. + Ok(()) + .tap(|_| drop(node)) + .tap(|_| drop(storage)) + .tap(|_| drop(guard)) +} From b6fe24f0a9c5e917d3f35cf1b5033e7e25fb7c23 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Thu, 11 Apr 2024 17:55:08 -0400 Subject: [PATCH 089/168] =?UTF-8?q?tests(app):=20=F0=9F=92=8E=20polish=20m?= =?UTF-8?q?ock=20consensus=20test=20infrastructure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit see #3588. follows #4184 and #4181. this takes a pass through the shared, Penumbra-specific test infrastructure for mock consensus tests. notably, this decomposes `init_chain.rs`, which has now become somewhat redundant with the existence of other more involved tests of e.g. validator uptime tracking. this also cleans up some unused imports, guards against future occurrences of that issue (_sharing code in `tests/` files is awkward_), and decomposes the `common/mod.rs` file into some distinct standalone components. this also belatedly removes the `common::start_test_node()` helper. at some point (_i was unable to find the link_) it was suggested that we refrain from a shared setup helper like that. this branch removes that helper, and updates its call-sites. this branch is largely code motion, and is intended to be a last bit of cleanup as we prepare for #3588 to wind down. :heart: --------- Co-authored-by: Henry de Valence --- .../app_can_spend_notes_and_detect_outputs.rs | 19 ++- crates/core/app/tests/common/mod.rs | 139 ++---------------- .../core/app/tests/common/temp_storage_ext.rs | 33 +++++ .../app/tests/common/test_node_builder_ext.rs | 11 +- crates/core/app/tests/common/test_node_ext.rs | 45 ++++++ .../app/tests/common/tracing_subscriber.rs | 29 ++++ crates/core/app/tests/init_chain.rs | 85 ----------- ...onsensus_can_define_a_genesis_validator.rs | 58 ++++++++ ...sus_can_send_a_sequence_of_empty_blocks.rs | 43 ++++++ 9 files changed, 244 insertions(+), 218 deletions(-) create mode 100644 crates/core/app/tests/common/temp_storage_ext.rs create mode 100644 crates/core/app/tests/common/test_node_ext.rs create mode 100644 crates/core/app/tests/common/tracing_subscriber.rs delete mode 100644 crates/core/app/tests/init_chain.rs create mode 100644 crates/core/app/tests/mock_consensus_can_define_a_genesis_validator.rs create mode 100644 crates/core/app/tests/mock_consensus_can_send_a_sequence_of_empty_blocks.rs diff --git a/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs b/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs index 580145a74f..d4bf8577d1 100644 --- a/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs +++ b/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs @@ -1,8 +1,8 @@ -mod common; - use { + self::common::BuilderExt, anyhow::anyhow, cnidarium::TempStorage, + penumbra_app::{genesis::AppState, server::consensus::Consensus}, penumbra_keys::test_keys, penumbra_mock_client::MockClient, penumbra_mock_consensus::TestNode, @@ -13,16 +13,27 @@ use { memo::MemoPlaintext, plan::MemoPlan, TransactionParameters, TransactionPlan, }, rand_core::OsRng, - tap::Tap, + tap::{Tap, TapFallible}, tracing::info, }; +mod common; + #[tokio::test] async fn app_can_spend_notes_and_detect_outputs() -> anyhow::Result<()> { // Install a test logger, acquire some temporary storage, and start the test node. let guard = common::set_tracing_subscriber(); let storage = TempStorage::new().await?; - let mut test_node = common::start_test_node(&storage).await?; + let mut test_node = { + let app_state = AppState::default(); + let consensus = Consensus::new(storage.as_ref().clone()); + TestNode::builder() + .single_validator() + .with_penumbra_auto_app_state(app_state)? + .init_chain(consensus) + .await + .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))? + }; // Sync the mock client, using the test wallet's spend key, to the latest snapshot. let mut client = MockClient::new(test_keys::SPEND_KEY.clone()) diff --git a/crates/core/app/tests/common/mod.rs b/crates/core/app/tests/common/mod.rs index 043d3d39f0..b1b00df1a8 100644 --- a/crates/core/app/tests/common/mod.rs +++ b/crates/core/app/tests/common/mod.rs @@ -1,20 +1,10 @@ //! Shared integration testing facilities. -// NB: Allow dead code, and unused imports. these are shared and consumed by files in `tests/`. -#![allow(dead_code, unused_imports)] - -pub use self::test_node_builder_ext::BuilderExt; - -use { - async_trait::async_trait, - cnidarium::TempStorage, - penumbra_app::{ - app::App, - genesis::AppState, - server::consensus::{Consensus, ConsensusService}, - }, - penumbra_mock_consensus::TestNode, - std::ops::Deref, +// NB: these reëxports are shared and consumed by files in `tests/`. +#[allow(unused_imports)] +pub use self::{ + temp_storage_ext::TempStorageExt, test_node_builder_ext::BuilderExt, + test_node_ext::TestNodeExt, tracing_subscriber::set_tracing_subscriber, }; /// Penumbra-specific extensions to the mock consensus builder. @@ -22,113 +12,16 @@ use { /// See [`BuilderExt`]. mod test_node_builder_ext; -// Installs a tracing subscriber to log events until the returned guard is dropped. -pub fn set_tracing_subscriber() -> tracing::subscriber::DefaultGuard { - use tracing_subscriber::filter::EnvFilter; - - let filter = "info,penumbra_app=trace,penumbra_mock_consensus=trace"; - let filter = EnvFilter::try_from_default_env() - .or_else(|_| EnvFilter::try_new(filter)) - .expect("should have a valid filter directive") - // Without explicitly disabling the `r1cs` target, the ZK proof implementations - // will spend an enormous amount of CPU and memory building useless tracing output. - .add_directive( - "r1cs=off" - .parse() - .expect("rics=off is a valid filter directive"), - ); - - let subscriber = tracing_subscriber::fmt() - .with_env_filter(filter) - .pretty() - .with_test_writer() - .finish(); - - tracing::subscriber::set_default(subscriber) -} - -/// A [`TestNode`] coupled with Penumbra's [`Consensus`] service. -pub type PenumbraTestNode = TestNode; - -/// Returns a new [`PenumbraTestNode`] backed by the given temporary storage. -pub async fn start_test_node(storage: &TempStorage) -> anyhow::Result { - use tap::TapFallible; - let app_state = AppState::default(); - let consensus = Consensus::new(storage.as_ref().clone()); - TestNode::builder() - .single_validator() - .with_penumbra_auto_app_state(app_state)? - .init_chain(consensus) - .await - .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain")) -} - -#[async_trait] -pub trait TempStorageExt: Sized { - async fn apply_genesis(self, genesis: AppState) -> anyhow::Result; - async fn apply_default_genesis(self) -> anyhow::Result; -} +/// Extensions to [`TempStorage`][cnidarium::TempStorage]. +mod temp_storage_ext; -#[async_trait] -impl TempStorageExt for TempStorage { - async fn apply_genesis(self, genesis: AppState) -> anyhow::Result { - // Check that we haven't already applied a genesis state: - if self.latest_version() != u64::MAX { - anyhow::bail!("database already initialized"); - } - - // Apply the genesis state to the storage - let mut app = App::new(self.latest_snapshot()).await?; - app.init_chain(&genesis).await; - app.commit(self.deref().clone()).await; - - Ok(self) - } - - async fn apply_default_genesis(self) -> anyhow::Result { - self.apply_genesis(Default::default()).await - } -} - -#[async_trait] -pub trait TestNodeExt: Sized { - async fn fast_forward_to_next_epoch( - &mut self, - storage: &TempStorage, - ) -> anyhow::Result; -} - -#[async_trait] -impl TestNodeExt for TestNode -where - C: tower::Service< - tendermint::v0_37::abci::ConsensusRequest, - Response = tendermint::v0_37::abci::ConsensusResponse, - Error = tower::BoxError, - > + Send - + Clone - + 'static, - C::Future: Send + 'static, - C::Error: Sized, -{ - async fn fast_forward_to_next_epoch( - &mut self, - storage: &TempStorage, - ) -> Result { - use {penumbra_sct::component::clock::EpochRead, tap::Tap}; - - let get_epoch = || async { storage.latest_snapshot().get_current_epoch().await }; - let start = get_epoch() - .await? - .tap(|start| tracing::info!(?start, "fast forwarding to next epoch")); +/// Penumbra-specific extensions to the mock consensus test node. +/// +/// See [`TestNodeExt`]. +mod test_node_ext; - loop { - self.block().execute().await?; - let current = get_epoch().await?; - if current != start { - tracing::debug!(end = ?current, ?start, "reached next epoch"); - return Ok(current); - } - } - } -} +/// A pretty [`tracing`] subscriber for use in test cases. +/// +/// NB: this subscriber makes use of a test writer, that is compatible with `cargo test`'s output +/// capturing. +mod tracing_subscriber; diff --git a/crates/core/app/tests/common/temp_storage_ext.rs b/crates/core/app/tests/common/temp_storage_ext.rs new file mode 100644 index 0000000000..5ae1524e03 --- /dev/null +++ b/crates/core/app/tests/common/temp_storage_ext.rs @@ -0,0 +1,33 @@ +use { + async_trait::async_trait, + cnidarium::TempStorage, + penumbra_app::{app::App, genesis::AppState}, + std::ops::Deref, +}; + +#[async_trait] +pub trait TempStorageExt: Sized { + async fn apply_genesis(self, genesis: AppState) -> anyhow::Result; + async fn apply_default_genesis(self) -> anyhow::Result; +} + +#[async_trait] +impl TempStorageExt for TempStorage { + async fn apply_genesis(self, genesis: AppState) -> anyhow::Result { + // Check that we haven't already applied a genesis state: + if self.latest_version() != u64::MAX { + anyhow::bail!("database already initialized"); + } + + // Apply the genesis state to the storage + let mut app = App::new(self.latest_snapshot()).await?; + app.init_chain(&genesis).await; + app.commit(self.deref().clone()).await; + + Ok(self) + } + + async fn apply_default_genesis(self) -> anyhow::Result { + self.apply_genesis(Default::default()).await + } +} diff --git a/crates/core/app/tests/common/test_node_builder_ext.rs b/crates/core/app/tests/common/test_node_builder_ext.rs index 0e586dfdcd..b074232cb5 100644 --- a/crates/core/app/tests/common/test_node_builder_ext.rs +++ b/crates/core/app/tests/common/test_node_builder_ext.rs @@ -1,11 +1,16 @@ use { + decaf377_rdsa::VerificationKey, penumbra_app::genesis::AppState, + penumbra_keys::keys::{SpendKey, SpendKeyBytes}, penumbra_mock_consensus::builder::Builder, penumbra_proto::{ core::keys::v1::{GovernanceKey, IdentityKey}, penumbra::core::component::stake::v1::Validator as PenumbraValidator, }, penumbra_shielded_pool::genesis::Allocation, + penumbra_stake::DelegationToken, + rand::Rng, + rand_core::OsRng, tracing::trace, }; @@ -57,12 +62,6 @@ impl BuilderExt for Builder { fn generate_penumbra_validator( consensus_key: &ed25519_consensus::VerificationKey, ) -> (PenumbraValidator, Allocation) { - use decaf377_rdsa::VerificationKey; - use penumbra_keys::keys::{SpendKey, SpendKeyBytes}; - use penumbra_stake::DelegationToken; - use rand::Rng; - use rand_core::OsRng; - let seed = SpendKeyBytes(OsRng.gen()); let spend_key = SpendKey::from(seed.clone()); let validator_id_sk = spend_key.spend_auth_key(); diff --git a/crates/core/app/tests/common/test_node_ext.rs b/crates/core/app/tests/common/test_node_ext.rs new file mode 100644 index 0000000000..48a7f55fd2 --- /dev/null +++ b/crates/core/app/tests/common/test_node_ext.rs @@ -0,0 +1,45 @@ +use { + async_trait::async_trait, cnidarium::TempStorage, penumbra_mock_consensus::TestNode, + penumbra_sct::component::clock::EpochRead as _, tap::Tap, +}; + +#[async_trait] +pub trait TestNodeExt: Sized { + async fn fast_forward_to_next_epoch( + &mut self, + storage: &TempStorage, + ) -> anyhow::Result; +} + +#[async_trait] +impl TestNodeExt for TestNode +where + C: tower::Service< + tendermint::v0_37::abci::ConsensusRequest, + Response = tendermint::v0_37::abci::ConsensusResponse, + Error = tower::BoxError, + > + Send + + Clone + + 'static, + C::Future: Send + 'static, + C::Error: Sized, +{ + async fn fast_forward_to_next_epoch( + &mut self, + storage: &TempStorage, + ) -> Result { + let get_epoch = || async { storage.latest_snapshot().get_current_epoch().await }; + let start = get_epoch() + .await? + .tap(|start| tracing::info!(?start, "fast forwarding to next epoch")); + + loop { + self.block().execute().await?; + let current = get_epoch().await?; + if current != start { + tracing::debug!(end = ?current, ?start, "reached next epoch"); + return Ok(current); + } + } + } +} diff --git a/crates/core/app/tests/common/tracing_subscriber.rs b/crates/core/app/tests/common/tracing_subscriber.rs new file mode 100644 index 0000000000..52597956d7 --- /dev/null +++ b/crates/core/app/tests/common/tracing_subscriber.rs @@ -0,0 +1,29 @@ +use { + tracing::subscriber::{set_default, DefaultGuard}, + tracing_subscriber::{filter::EnvFilter, fmt}, +}; + +/// Installs a tracing subscriber to log events until the returned guard is dropped. +// NB: this is marked as "dead code" but it is used by integration tests. +#[allow(dead_code)] +pub fn set_tracing_subscriber() -> DefaultGuard { + let filter = "info,penumbra_app=trace,penumbra_mock_consensus=trace"; + let filter = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new(filter)) + .expect("should have a valid filter directive") + // Without explicitly disabling the `r1cs` target, the ZK proof implementations + // will spend an enormous amount of CPU and memory building useless tracing output. + .add_directive( + "r1cs=off" + .parse() + .expect("rics=off is a valid filter directive"), + ); + + let subscriber = fmt() + .with_env_filter(filter) + .pretty() + .with_test_writer() + .finish(); + + set_default(subscriber) +} diff --git a/crates/core/app/tests/init_chain.rs b/crates/core/app/tests/init_chain.rs deleted file mode 100644 index b983f3c420..0000000000 --- a/crates/core/app/tests/init_chain.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! App integration tests using mock consensus. -// -// Note: these should eventually replace the existing test cases. mock consensus tests are placed -// here while the engine is still in development. See #3588. - -mod common; - -use { - anyhow::anyhow, cnidarium::TempStorage, penumbra_sct::component::clock::EpochRead, - penumbra_stake::component::validator_handler::ValidatorDataRead as _, tap::Tap, tracing::info, -}; - -/// Exercises that a test node can be instantiated using the consensus service. -#[tokio::test] -async fn mock_consensus_can_send_an_init_chain_request() -> anyhow::Result<()> { - // Install a test logger, acquire some temporary storage, and start the test node. - let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; - let _ = common::start_test_node(&storage).await?; - - // Free our temporary storage. - drop(storage); - drop(guard); - - Ok(()) -} - -/// Exercises that the mock consensus engine can provide a single genesis validator. -#[tokio::test] -async fn mock_consensus_can_define_a_genesis_validator() -> anyhow::Result<()> { - // Install a test logger, acquire some temporary storage, and start the test node. - let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; - let _test_node = common::start_test_node(&storage).await?; - - let snapshot = storage.latest_snapshot(); - let validators = snapshot - .validator_definitions() - .tap(|_| info!("getting validator definitions")) - .await?; - match validators.as_slice() { - [v] => { - let identity_key = v.identity_key; - let status = snapshot - .get_validator_state(&identity_key) - .await? - .ok_or_else(|| anyhow!("could not find validator status"))?; - assert_eq!( - status, - penumbra_stake::validator::State::Active, - "validator should be active" - ); - } - unexpected => panic!("there should be one validator, got: {unexpected:?}"), - } - - // Free our temporary storage. - drop(storage); - drop(guard); - - Ok(()) -} - -/// Exercises that a series of empty blocks, with no validator set present, can be successfully -/// executed by the consensus service. -#[tokio::test] -async fn mock_consensus_can_send_a_sequence_of_empty_blocks() -> anyhow::Result<()> { - // Install a test logger, acquire some temporary storage, and start the test node. - let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; - let mut test_node = common::start_test_node(&storage).await?; - - let height = || async { storage.latest_snapshot().get_block_height().await }; - - // Fast forward eight blocks, and show that the height is 8 after doing so. - assert_eq!(height().await?, 0, "height should begin at 0"); - test_node.fast_forward(8).await?; - assert_eq!(height().await?, 8_u64, "height should grow"); - - // Free our temporary storage. - drop(storage); - drop(guard); - - Ok(()) -} diff --git a/crates/core/app/tests/mock_consensus_can_define_a_genesis_validator.rs b/crates/core/app/tests/mock_consensus_can_define_a_genesis_validator.rs new file mode 100644 index 0000000000..e1954bc743 --- /dev/null +++ b/crates/core/app/tests/mock_consensus_can_define_a_genesis_validator.rs @@ -0,0 +1,58 @@ +use { + self::common::BuilderExt, + anyhow::anyhow, + cnidarium::TempStorage, + penumbra_app::{genesis::AppState, server::consensus::Consensus}, + penumbra_mock_consensus::TestNode, + penumbra_stake::component::validator_handler::ValidatorDataRead as _, + tap::{Tap, TapFallible}, + tracing::info, +}; + +mod common; + +/// Exercises that the mock consensus engine can provide a single genesis validator. +#[tokio::test] +async fn mock_consensus_can_define_a_genesis_validator() -> anyhow::Result<()> { + // Install a test logger, acquire some temporary storage, and start the test node. + let guard = common::set_tracing_subscriber(); + let storage = TempStorage::new().await?; + let test_node = { + let app_state = AppState::default(); + let consensus = Consensus::new(storage.as_ref().clone()); + TestNode::builder() + .single_validator() + .with_penumbra_auto_app_state(app_state)? + .init_chain(consensus) + .await + .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))? + }; + + let snapshot = storage.latest_snapshot(); + let validators = snapshot + .validator_definitions() + .tap(|_| info!("getting validator definitions")) + .await?; + match validators.as_slice() { + [v] => { + let identity_key = v.identity_key; + let status = snapshot + .get_validator_state(&identity_key) + .await? + .ok_or_else(|| anyhow!("could not find validator status"))?; + assert_eq!( + status, + penumbra_stake::validator::State::Active, + "validator should be active" + ); + } + unexpected => panic!("there should be one validator, got: {unexpected:?}"), + } + + // Free our temporary storage. + drop(test_node); + drop(storage); + drop(guard); + + Ok(()) +} diff --git a/crates/core/app/tests/mock_consensus_can_send_a_sequence_of_empty_blocks.rs b/crates/core/app/tests/mock_consensus_can_send_a_sequence_of_empty_blocks.rs new file mode 100644 index 0000000000..74e662285b --- /dev/null +++ b/crates/core/app/tests/mock_consensus_can_send_a_sequence_of_empty_blocks.rs @@ -0,0 +1,43 @@ +use { + self::common::BuilderExt, + cnidarium::TempStorage, + penumbra_app::{genesis::AppState, server::consensus::Consensus}, + penumbra_mock_consensus::TestNode, + penumbra_sct::component::clock::EpochRead as _, + tap::TapFallible, +}; + +mod common; + +/// Exercises that a series of empty blocks, with no validator set present, can be successfully +/// executed by the consensus service. +#[tokio::test] +async fn mock_consensus_can_send_a_sequence_of_empty_blocks() -> anyhow::Result<()> { + // Install a test logger, acquire some temporary storage, and start the test node. + let guard = common::set_tracing_subscriber(); + let storage = TempStorage::new().await?; + let mut test_node = { + let app_state = AppState::default(); + let consensus = Consensus::new(storage.as_ref().clone()); + TestNode::builder() + .single_validator() + .with_penumbra_auto_app_state(app_state)? + .init_chain(consensus) + .await + .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))? + }; + + let height = || async { storage.latest_snapshot().get_block_height().await }; + + // Fast forward eight blocks, and show that the height is 8 after doing so. + assert_eq!(height().await?, 0, "height should begin at 0"); + test_node.fast_forward(8).await?; + assert_eq!(height().await?, 8_u64, "height should grow"); + + // Free our temporary storage. + drop(test_node); + drop(storage); + drop(guard); + + Ok(()) +} From f0dba4cf437a1a4836da827f6034b00c36a15f8b Mon Sep 17 00:00:00 2001 From: Conor Schaefer Date: Thu, 11 Apr 2024 14:58:52 -0700 Subject: [PATCH 090/168] ci: update several eol actions Changes made: * docker/login-action v2 -> v3 * docker/metadata-action v4 -> v5 * docker/build-push-action v3 -> v5 * get-gke-credentials v1 -> v2 * helmfile v1 -> v2 * migrate actions-rs/toolchain -> dtolnay/rust-toolchain * remove docker/setup-qemu-action * remove battila7/get-version-action * removes unused container builds All of these were showing warnings. These upgrades *should* have zero side-effects for our CI, but I'll check post-merge and confirm that. Refs #3769, but does not close it: follow-up may be necessary post-merge, plus corresponding updates should be made across our repos. --- .github/workflows/buf-pull-request.yml | 8 +-- .github/workflows/containers.yml | 79 +++----------------------- .github/workflows/deploy-preview.yml | 4 +- .github/workflows/deploy-testnet.yml | 4 +- .github/workflows/disable-faucet.yml | 2 +- .github/workflows/notes.yml | 16 +++--- .github/workflows/smoke.yml | 11 ++-- .github/workflows/summoner_smoke.yml | 8 +-- 8 files changed, 34 insertions(+), 98 deletions(-) diff --git a/.github/workflows/buf-pull-request.yml b/.github/workflows/buf-pull-request.yml index ec58046a07..627c668fe7 100644 --- a/.github/workflows/buf-pull-request.yml +++ b/.github/workflows/buf-pull-request.yml @@ -54,12 +54,8 @@ jobs: with: lfs: true - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: false + - name: Install rust toolchain + uses: dtolnay/rust-toolchain@stable - uses: bufbuild/buf-setup-action@v1 with: diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml index db93f32a4e..02b7074b9a 100644 --- a/.github/workflows/containers.yml +++ b/.github/workflows/containers.yml @@ -23,32 +23,29 @@ jobs: lfs: true - name: Log in to the Docker Hub container registry (for pulls) - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Log in to the GitHub container registry (for pushes) - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: ghcr.io/penumbra-zone/penumbra - name: Build and push Docker image - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64 @@ -69,13 +66,13 @@ jobs: uses: actions/checkout@v4 - name: Log in to the Docker Hub container registry (for pulls) - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Log in to the GitHub container registry (for pushes) - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} @@ -83,12 +80,12 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: ghcr.io/penumbra-zone/grafana - name: Build and push Docker image - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64 @@ -97,27 +94,6 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - osiris: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - needs: - - penumbra - steps: - - name: Checkout repository - uses: actions/checkout@v4 - # We use the GHA Repository Dispatch functionality to trigger a container - # build in the penumbra-zone/osiris repo. - - name: Trigger remote build - shell: bash - env: - GITHUB_PAT: ${{ secrets.GH_PAT }} - run: |- - export PENUMBRA_VERSION='${{ github.event.inputs.image_tag || github.ref_name }}' - cd deployments/ - ./scripts/gha-repository-dispatch penumbra-zone/osiris - galileo: runs-on: ubuntu-latest permissions: @@ -145,42 +121,3 @@ jobs: fi cd deployments/ ./scripts/gha-repository-dispatch penumbra-zone/galileo - - relayer: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Log in to the Docker Hub container registry (for pulls) - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Log in to the GitHub container registry (for pushes) - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v4 - with: - images: ghcr.io/penumbra-zone/relayer - - - name: Build and push Docker image - uses: docker/build-push-action@v3 - with: - context: . - platforms: linux/amd64 - file: deployments/containerfiles/Dockerfile-relayer - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index 37a573c7a6..1e563b8d5f 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -50,14 +50,14 @@ jobs: service_account: 'github-actions@penumbra-sl-testnet.iam.gserviceaccount.com' - name: get gke credentials - uses: google-github-actions/get-gke-credentials@v1 + uses: google-github-actions/get-gke-credentials@v2 with: cluster_name: testnet project_id: penumbra-sl-testnet location: us-central1 - name: install helmfile - uses: mamezou-tech/setup-helmfile@v1.3.0 + uses: mamezou-tech/setup-helmfile@v2.0.0 with: helmfile-version: "v0.157.0" diff --git a/.github/workflows/deploy-testnet.yml b/.github/workflows/deploy-testnet.yml index dc5b6c9fca..b4f5dc885e 100644 --- a/.github/workflows/deploy-testnet.yml +++ b/.github/workflows/deploy-testnet.yml @@ -42,14 +42,14 @@ jobs: service_account: 'github-actions@penumbra-sl-testnet.iam.gserviceaccount.com' - name: get gke credentials - uses: google-github-actions/get-gke-credentials@v1 + uses: google-github-actions/get-gke-credentials@v2 with: cluster_name: testnet project_id: penumbra-sl-testnet location: us-central1 - name: install helmfile - uses: mamezou-tech/setup-helmfile@v1.3.0 + uses: mamezou-tech/setup-helmfile@v2.0.0 with: helmfile-version: "v0.157.0" diff --git a/.github/workflows/disable-faucet.yml b/.github/workflows/disable-faucet.yml index fdd701e38e..bd8c2508b2 100644 --- a/.github/workflows/disable-faucet.yml +++ b/.github/workflows/disable-faucet.yml @@ -28,7 +28,7 @@ jobs: service_account: 'github-actions@penumbra-sl-testnet.iam.gserviceaccount.com' - name: get gke credentials - uses: google-github-actions/get-gke-credentials@v1 + uses: google-github-actions/get-gke-credentials@v2 with: cluster_name: testnet project_id: penumbra-sl-testnet diff --git a/.github/workflows/notes.yml b/.github/workflows/notes.yml index 26c4faf81e..1a9eff4789 100644 --- a/.github/workflows/notes.yml +++ b/.github/workflows/notes.yml @@ -21,11 +21,11 @@ jobs: uses: dtolnay/rust-toolchain@nightly - name: Load Rust caching uses: astriaorg/buildjet-rust-cache@v2.5.1 - - name: Load get-version action to grab version component of deployment path - uses: battila7/get-version-action@v2 - id: get_version + + # Previously we used a GHA helper to look up the version, which was overkill. + # Let's still log the version of the docs we intend to build. - name: Print version component of deployment path - run: echo ${{ steps.get_version.outputs.version }} + run: echo ${{ github.event.inputs.image_tag || github.ref_name }} # Ostensibly building from source, but the cache-loading above # ensures we don't need to rebuild frequently. @@ -41,7 +41,7 @@ jobs: cd docs/guide rm -rf firebase-tmp mkdir firebase-tmp - mv book/html firebase-tmp/${{ steps.get_version.outputs.version }} + mv book/html firebase-tmp/${{ github.event.inputs.image_tag || github.ref_name }} tree firebase-tmp - name: Deploy software guide to firebase @@ -61,7 +61,7 @@ jobs: cd docs/protocol rm -rf firebase-tmp mkdir firebase-tmp - mv book/html firebase-tmp/${{ steps.get_version.outputs.version }} + mv book/html firebase-tmp/${{ github.event.inputs.image_tag || github.ref_name }} tree firebase-tmp - name: Deploy protocol spec to firebase @@ -80,9 +80,9 @@ jobs: cd docs/rustdoc if [ -d "firebase-tmp" ]; then rm -rf firebase-tmp; fi mkdir firebase-tmp - mv ../../target/doc firebase-tmp/${{ steps.get_version.outputs.version }} + mv ../../target/doc firebase-tmp/${{ github.event.inputs.image_tag || github.ref_name }} # Copy in the static index file - cp index.html firebase-tmp/${{ steps.get_version.outputs.version }} + cp index.html firebase-tmp/${{ github.event.inputs.image_tag || github.ref_name }} - name: Deploy API docs to firebase uses: w9jds/firebase-action@v12.9.0 diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 68af227f65..2f9dc7035a 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -16,18 +16,21 @@ jobs: - uses: actions/checkout@v4 with: lfs: true - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable + + - name: Install rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Load rust cache uses: astriaorg/buildjet-rust-cache@v2.5.1 + - name: Install cometbft binary run: ./deployments/scripts/install-cometbft + - name: Run the smoke test suite run: | export PATH="$HOME/bin:$PATH" ./deployments/scripts/smoke-test.sh + - name: Display comet logs if: always() run: cat deployments/logs/comet.log diff --git a/.github/workflows/summoner_smoke.yml b/.github/workflows/summoner_smoke.yml index c9fed06b8c..6c535c13b7 100644 --- a/.github/workflows/summoner_smoke.yml +++ b/.github/workflows/summoner_smoke.yml @@ -19,10 +19,10 @@ jobs: - uses: actions/checkout@v4 with: lfs: true - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable + + - name: Install rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Load rust cache uses: astriaorg/buildjet-rust-cache@v2.5.1 From 24bcfbd58723b5799083ca24ebbbe3dde1592f46 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Fri, 12 Apr 2024 09:23:36 -0400 Subject: [PATCH 091/168] =?UTF-8?q?feat(ci):=20=F0=9F=94=96=20add=20`needs?= =?UTF-8?q?-refinement`=20label=20to=20new=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the Penumbra team meets regularly to refine and discuss our backlog of tasks. this adds a workflow so that newly opened issues are accordingly labeled `needs-refinement`, so that they can be easily filtered in our refinement meeting. this workflow is heavily inspired by the example workflow shown here: https://docs.github.com/en/actions/managing-issues-and-pull-requests/adding-labels-to-issues --- .../new-issues-labeled-needs-refinement.yml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/new-issues-labeled-needs-refinement.yml diff --git a/.github/workflows/new-issues-labeled-needs-refinement.yml b/.github/workflows/new-issues-labeled-needs-refinement.yml new file mode 100644 index 0000000000..951ba719cc --- /dev/null +++ b/.github/workflows/new-issues-labeled-needs-refinement.yml @@ -0,0 +1,20 @@ +# Adds the `needs-refinement` label to newly opened issues. +name: New issues need refinement +on: + issues: + types: + - opened +jobs: + label_issues: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - run: gh issue edit "$NUMBER" --add-label "$LABELS" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + NUMBER: ${{ github.event.issue.number }} + # Separate multiple labels with commas if other labels are ever + # needed e.g., `needs-refinement,foo`. + LABELS: needs-refinement From de66328c70b239e2d51016db769667040871a3f2 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Fri, 12 Apr 2024 13:23:42 -0400 Subject: [PATCH 092/168] =?UTF-8?q?tests:=20=F0=9F=A4=9D=20hoist=20test=20?= =?UTF-8?q?subscriber=20into=20a=20standalone=20library=20(#4200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit in #4193, @zbuc is working on some changes to the dex component. in the `penumbra-app` crate, we have a helpful utility to define a tracing subscriber that correctly interacts with `libtest`'s output capturing, and can be configured via `RUST_LOG` when running tests. this is a tremendously helpful aid in debugging issues during feature development, and is not specific to the `penumbra-app` crate. this commit hoists that test utility into a standalone library in `crates/test/`, so that other components like the dex can also create a guard to capture traces in tests. now we can share this, by doing this: ```rust #[tokio::test] async fn example_test_case() -> anyhow::Result<()> { // Install a test logger... let guard = set_tracing_subscriber(); // Test logic here... drop(guard); Ok(()) } ``` #### checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > only changes test code --- Cargo.lock | 9 +++++++++ Cargo.toml | 2 ++ crates/core/app/Cargo.toml | 17 +++++++++-------- crates/core/app/tests/common/mod.rs | 15 ++++++--------- crates/test/tracing-subscriber/Cargo.toml | 13 +++++++++++++ .../tracing-subscriber/src/lib.rs} | 5 +++++ 6 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 crates/test/tracing-subscriber/Cargo.toml rename crates/{core/app/tests/common/tracing_subscriber.rs => test/tracing-subscriber/src/lib.rs} (85%) diff --git a/Cargo.lock b/Cargo.lock index 748816d82e..7f1e79d1ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4652,6 +4652,7 @@ dependencies = [ "penumbra-stake", "penumbra-tct", "penumbra-tendermint-proxy", + "penumbra-test-subscriber", "penumbra-tower-trace", "penumbra-transaction", "penumbra-txhash", @@ -5577,6 +5578,14 @@ dependencies = [ "url", ] +[[package]] +name = "penumbra-test-subscriber" +version = "0.71.0" +dependencies = [ + "tracing", + "tracing-subscriber 0.3.18", +] + [[package]] name = "penumbra-tower-trace" version = "0.71.0" diff --git a/Cargo.toml b/Cargo.toml index a7dc78e149..2c84b5b206 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ members = [ "crates/test/mock-client", "crates/test/mock-consensus", "crates/test/tct-property-test", + "crates/test/tracing-subscriber", "crates/util/auto-https", "crates/util/tendermint-proxy", "crates/util/tower-trace", @@ -179,6 +180,7 @@ penumbra-sct = { default-features = false, path = "crates/co penumbra-shielded-pool = { default-features = false, path = "crates/core/component/shielded-pool" } penumbra-stake = { default-features = false, path = "crates/core/component/stake" } penumbra-tct = { default-features = false, path = "crates/crypto/tct" } +penumbra-test-subscriber = { path = "crates/test/tracing-subscriber" } penumbra-transaction = { default-features = false, path = "crates/core/transaction" } penumbra-txhash = { default-features = false, path = "crates/core/txhash" } penumbra-view = { path = "crates/view" } diff --git a/crates/core/app/Cargo.toml b/crates/core/app/Cargo.toml index 8c04f1d87f..3210cde095 100644 --- a/crates/core/app/Cargo.toml +++ b/crates/core/app/Cargo.toml @@ -81,14 +81,15 @@ tracing = { workspace = true } url = { workspace = true } [dev-dependencies] -ed25519-consensus = { workspace = true } -penumbra-mock-consensus = { workspace = true } -penumbra-mock-client = { workspace = true } -rand = { workspace = true } -rand_core = { workspace = true } -rand_chacha = { workspace = true } -tap = { workspace = true } -tracing-subscriber = { workspace = true } +ed25519-consensus = { workspace = true } +penumbra-mock-consensus = { workspace = true } +penumbra-mock-client = { workspace = true } +penumbra-test-subscriber = { workspace = true } +rand = { workspace = true } +rand_core = { workspace = true } +rand_chacha = { workspace = true } +tap = { workspace = true } +tracing-subscriber = { workspace = true } # Enable the feature flags to get proving keys when running tests. [dev-dependencies.penumbra-proof-params] diff --git a/crates/core/app/tests/common/mod.rs b/crates/core/app/tests/common/mod.rs index b1b00df1a8..1a6e788508 100644 --- a/crates/core/app/tests/common/mod.rs +++ b/crates/core/app/tests/common/mod.rs @@ -2,9 +2,12 @@ // NB: these reëxports are shared and consumed by files in `tests/`. #[allow(unused_imports)] -pub use self::{ - temp_storage_ext::TempStorageExt, test_node_builder_ext::BuilderExt, - test_node_ext::TestNodeExt, tracing_subscriber::set_tracing_subscriber, +pub use { + self::{ + temp_storage_ext::TempStorageExt, test_node_builder_ext::BuilderExt, + test_node_ext::TestNodeExt, + }, + penumbra_test_subscriber::set_tracing_subscriber, }; /// Penumbra-specific extensions to the mock consensus builder. @@ -19,9 +22,3 @@ mod temp_storage_ext; /// /// See [`TestNodeExt`]. mod test_node_ext; - -/// A pretty [`tracing`] subscriber for use in test cases. -/// -/// NB: this subscriber makes use of a test writer, that is compatible with `cargo test`'s output -/// capturing. -mod tracing_subscriber; diff --git a/crates/test/tracing-subscriber/Cargo.toml b/crates/test/tracing-subscriber/Cargo.toml new file mode 100644 index 0000000000..f4be0e6d37 --- /dev/null +++ b/crates/test/tracing-subscriber/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "penumbra-test-subscriber" +authors.workspace = true +edition.workspace = true +version.workspace = true +repository.workspace = true +homepage.workspace = true +license.workspace = true +publish = false + +[dependencies] +tracing = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/crates/core/app/tests/common/tracing_subscriber.rs b/crates/test/tracing-subscriber/src/lib.rs similarity index 85% rename from crates/core/app/tests/common/tracing_subscriber.rs rename to crates/test/tracing-subscriber/src/lib.rs index 52597956d7..3e48b80c0a 100644 --- a/crates/core/app/tests/common/tracing_subscriber.rs +++ b/crates/test/tracing-subscriber/src/lib.rs @@ -1,3 +1,8 @@ +//! A pretty [`tracing`] subscriber for use in test cases. +//! +//! NB: this subscriber makes use of a test writer, that is compatible with `cargo test`'s output +//! capturing. + use { tracing::subscriber::{set_default, DefaultGuard}, tracing_subscriber::{filter::EnvFilter, fmt}, From 9d156d257534404fa26a0934b39b2ee0b601fabf Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Mon, 15 Apr 2024 07:22:51 -0400 Subject: [PATCH 093/168] auction: create protobuf module scaffolding (#4204) ## Describe your changes This adds a new protobuf module for the auction component at version `v1alpha1`, along with two elementary messages: - `AuctionId` a wrapper around a bytestring - `AuctionNft` which embeds an `AuctionId` and a sequence number along with a stub query service. ## Issue ticket number and link #4196 ## Checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > This adds a protobuf module. --- ...enumbra.core.component.auction.v1alpha1.rs | 283 +++++++++++++ ...a.core.component.auction.v1alpha1.serde.rs | 380 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 379727 -> 380907 bytes crates/proto/src/lib.rs | 6 + .../component/auction/v1alpha1/auction.proto | 26 ++ tools/proto-compiler/src/main.rs | 1 + 6 files changed, 696 insertions(+) create mode 100644 crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs create mode 100644 crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs create mode 100644 proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto diff --git a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs new file mode 100644 index 0000000000..cf2c59a013 --- /dev/null +++ b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs @@ -0,0 +1,283 @@ +/// A unique identifier for an auction, obtained from hashing a domain separator +/// along with the immutable part of an auction description. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AuctionId { + #[prost(bytes = "vec", tag = "1")] + pub inner: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for AuctionId { + const NAME: &'static str = "AuctionId"; + const PACKAGE: &'static str = "penumbra.core.component.auction.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.auction.v1alpha1.{}", Self::NAME + ) + } +} +/// A bearer NFT tracking ownership of an auction and its proceeds. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AuctionNft { + #[prost(message, optional, tag = "1")] + pub id: ::core::option::Option, + #[prost(uint64, tag = "2")] + pub seq: u64, +} +impl ::prost::Name for AuctionNft { + const NAME: &'static str = "AuctionNft"; + const PACKAGE: &'static str = "penumbra.core.component.auction.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.auction.v1alpha1.{}", Self::NAME + ) + } +} +/// The configuration parameters for the auction component. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AuctionParameters {} +impl ::prost::Name for AuctionParameters { + const NAME: &'static str = "AuctionParameters"; + const PACKAGE: &'static str = "penumbra.core.component.auction.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.auction.v1alpha1.{}", Self::NAME + ) + } +} +/// Genesis data for the auction component. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GenesisContent { + /// The configuration parameters for the auction component at genesis. + #[prost(message, optional, tag = "1")] + pub params: ::core::option::Option, +} +impl ::prost::Name for GenesisContent { + const NAME: &'static str = "GenesisContent"; + const PACKAGE: &'static str = "penumbra.core.component.auction.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.auction.v1alpha1.{}", Self::NAME + ) + } +} +/// Generated client implementations. +#[cfg(feature = "rpc")] +pub mod query_service_client { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// Query operations for the Auction component. + #[derive(Debug, Clone)] + pub struct QueryServiceClient { + inner: tonic::client::Grpc, + } + impl QueryServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl QueryServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> QueryServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + Send + Sync, + { + QueryServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + } +} +/// Generated server implementations. +#[cfg(feature = "rpc")] +pub mod query_service_server { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with QueryServiceServer. + #[async_trait] + pub trait QueryService: Send + Sync + 'static {} + /// Query operations for the Auction component. + #[derive(Debug)] + pub struct QueryServiceServer { + inner: _Inner, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + struct _Inner(Arc); + impl QueryServiceServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + let inner = _Inner(inner); + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for QueryServiceServer + where + T: QueryService, + B: Body + Send + 'static, + B::Error: Into + Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + let inner = self.inner.clone(); + match req.uri().path() { + _ => { + Box::pin(async move { + Ok( + http::Response::builder() + .status(200) + .header("grpc-status", "12") + .header("content-type", "application/grpc") + .body(empty_body()) + .unwrap(), + ) + }) + } + } + } + } + impl Clone for QueryServiceServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + impl Clone for _Inner { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } + } + impl std::fmt::Debug for _Inner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } + } + impl tonic::server::NamedService for QueryServiceServer { + const NAME: &'static str = "penumbra.core.component.auction.v1alpha1.QueryService"; + } +} diff --git a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs new file mode 100644 index 0000000000..603d1ccf95 --- /dev/null +++ b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs @@ -0,0 +1,380 @@ +impl serde::Serialize for AuctionId { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.inner.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.auction.v1alpha1.AuctionId", len)?; + if !self.inner.is_empty() { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("inner", pbjson::private::base64::encode(&self.inner).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for AuctionId { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "inner", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Inner, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "inner" => Ok(GeneratedField::Inner), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = AuctionId; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.auction.v1alpha1.AuctionId") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut inner__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Inner => { + if inner__.is_some() { + return Err(serde::de::Error::duplicate_field("inner")); + } + inner__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(AuctionId { + inner: inner__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.auction.v1alpha1.AuctionId", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for AuctionNft { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.id.is_some() { + len += 1; + } + if self.seq != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.auction.v1alpha1.AuctionNft", len)?; + if let Some(v) = self.id.as_ref() { + struct_ser.serialize_field("id", v)?; + } + if self.seq != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("seq", ToString::to_string(&self.seq).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for AuctionNft { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "id", + "seq", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Id, + Seq, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "id" => Ok(GeneratedField::Id), + "seq" => Ok(GeneratedField::Seq), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = AuctionNft; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.auction.v1alpha1.AuctionNft") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut id__ = None; + let mut seq__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Id => { + if id__.is_some() { + return Err(serde::de::Error::duplicate_field("id")); + } + id__ = map_.next_value()?; + } + GeneratedField::Seq => { + if seq__.is_some() { + return Err(serde::de::Error::duplicate_field("seq")); + } + seq__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(AuctionNft { + id: id__, + seq: seq__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.auction.v1alpha1.AuctionNft", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for AuctionParameters { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let len = 0; + let struct_ser = serializer.serialize_struct("penumbra.core.component.auction.v1alpha1.AuctionParameters", len)?; + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for AuctionParameters { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + Ok(GeneratedField::__SkipField__) + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = AuctionParameters; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.auction.v1alpha1.AuctionParameters") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + while map_.next_key::()?.is_some() { + let _ = map_.next_value::()?; + } + Ok(AuctionParameters { + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.auction.v1alpha1.AuctionParameters", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GenesisContent { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.params.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.auction.v1alpha1.GenesisContent", len)?; + if let Some(v) = self.params.as_ref() { + struct_ser.serialize_field("params", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GenesisContent { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "params", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Params, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "params" => Ok(GeneratedField::Params), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GenesisContent; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.auction.v1alpha1.GenesisContent") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut params__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Params => { + if params__.is_some() { + return Err(serde::de::Error::duplicate_field("params")); + } + params__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(GenesisContent { + params: params__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.auction.v1alpha1.GenesisContent", FIELDS, GeneratedVisitor) + } +} diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 5c5a38007f0f3ea943adfb4e1452c7cee9443fae..1e88dce399de2ad43e3e5009c5f29ec87633db74 100644 GIT binary patch delta 911 zcmZ`&F>ljA6!zJUNiLxzPSP}1p|2S#kZ7tRfyzeI0s@JOw$Pc2eQ8cKzSO?7LJVby z1Pd$?1SXh}SeTGtLLFEUKZK=!0Kq%r2q>zj`|f++_rCXZ{`R8s{dMJ0pY+YX^}&AQ z*sn75(xC8gYKrxIC1kj;yU4b?ZML|OTnyJX2~-kLAG5VtnrZa>!PkT7Qq89n&sx3O zQtmP}MKHMXdG_8>Q9Y@;5v*g0o2ORHccx4rXYF-;tQBb?DY?xe*5QgragEc7>pbMK zh?ixkc&OYB>U1?fJjhtJtH*WuW3fIek1tHn+**%E4>owTD|~+Qr9&OpHjHXzNPgPI z{YCnmP}4SCJ7t_8-V*{1=!IgZ$3XB8Xf-O~Oo%t>6E8YhX@z?eT@AXE(9Ncx-y zabi!!?m(zk>+Sh14pCm7!h~V%I0vJBho^$FxH_-&MYJpNokZGBh6)U zV}!EYsXSf5Nle$u8`Iw9;g3UeZa__EWLA?`g1Xrex!uF{LBuw1)nOcL+h;Mvp3 z`f&;i6~*)FEi4Pzm_G?}O?T91F>f#9X4ziG&02Z{0Qg}HVgLXD diff --git a/crates/proto/src/lib.rs b/crates/proto/src/lib.rs index da35abb829..4268d190ed 100644 --- a/crates/proto/src/lib.rs +++ b/crates/proto/src/lib.rs @@ -72,6 +72,12 @@ pub mod penumbra { /// Components of the Penumbra application. pub mod component { + pub mod auction { + pub mod v1alpha1 { + include!("gen/penumbra.core.component.auction.v1alpha1.rs"); + include!("gen/penumbra.core.component.auction.v1alpha1.serde.rs"); + } + } pub mod compact_block { pub mod v1 { include!("gen/penumbra.core.component.compact_block.v1.rs"); diff --git a/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto b/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto new file mode 100644 index 0000000000..f325110f25 --- /dev/null +++ b/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; +package penumbra.core.component.auction.v1alpha1; + +// A unique identifier for an auction, obtained from hashing a domain separator +// along with the immutable part of an auction description. +message AuctionId { + bytes inner = 1; +} + +// A bearer NFT tracking ownership of an auction and its proceeds. +message AuctionNft { + AuctionId id = 1; + uint64 seq = 2; +} + +// The configuration parameters for the auction component. +message AuctionParameters {} + +// Genesis data for the auction component. +message GenesisContent { + // The configuration parameters for the auction component at genesis. + AuctionParameters params = 1; +} + +// Query operations for the Auction component. +service QueryService {} diff --git a/tools/proto-compiler/src/main.rs b/tools/proto-compiler/src/main.rs index 1122df7db9..061daf217d 100644 --- a/tools/proto-compiler/src/main.rs +++ b/tools/proto-compiler/src/main.rs @@ -92,6 +92,7 @@ fn main() -> anyhow::Result<()> { "../../proto/penumbra/penumbra/core/txhash/v1/txhash.proto", "../../proto/penumbra/penumbra/core/component/compact_block/v1/compact_block.proto", "../../proto/penumbra/penumbra/core/component/community_pool/v1/community_pool.proto", + "../../proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto", "../../proto/penumbra/penumbra/core/component/dex/v1/dex.proto", "../../proto/penumbra/penumbra/core/component/distributions/v1/distributions.proto", "../../proto/penumbra/penumbra/core/component/funding/v1/funding.proto", From 60ecaf16c6ae501e0ca8190e6b1ab9399d82a027 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Mon, 15 Apr 2024 10:53:18 -0400 Subject: [PATCH 094/168] auction: create component scaffolding (#4205) ## Describe your changes This PR create scaffolding for an auction component, including structure for future `state_key`s, `event`s, and `rpc` definition. It also includes def -initions for elementary data structures like `AuctionId` and `AuctionNft`. ## Issue ticket number and link Part of #4196 ## Checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > This is not consensus breaking (yet) because the component is not used by the app. --- Cargo.lock | 55 +++++++++++++ Cargo.toml | 2 + crates/bin/pcli/Cargo.toml | 2 + crates/bin/pd/Cargo.toml | 1 + crates/core/component/auction/Cargo.toml | 82 +++++++++++++++++++ crates/core/component/auction/src/auction.rs | 2 + .../core/component/auction/src/auction/id.rs | 69 ++++++++++++++++ .../core/component/auction/src/auction/nft.rs | 41 ++++++++++ .../auction/src/component/action_handler.rs | 1 + .../auction/src/component/auction.rs | 57 +++++++++++++ .../auction/src/component/metrics.rs | 25 ++++++ .../component/auction/src/component/mod.rs | 6 ++ .../component/auction/src/component/rpc.rs | 25 ++++++ crates/core/component/auction/src/event.rs | 5 ++ crates/core/component/auction/src/genesis.rs | 37 +++++++++ crates/core/component/auction/src/lib.rs | 13 +++ crates/core/component/auction/src/params.rs | 25 ++++++ .../core/component/auction/src/state_key.rs | 42 ++++++++++ crates/core/transaction/Cargo.toml | 2 + crates/proto/src/serializers/bech32str.rs | 22 +++++ deployments/scripts/check-wasm-compat.sh | 1 + 21 files changed, 515 insertions(+) create mode 100644 crates/core/component/auction/Cargo.toml create mode 100644 crates/core/component/auction/src/auction.rs create mode 100644 crates/core/component/auction/src/auction/id.rs create mode 100644 crates/core/component/auction/src/auction/nft.rs create mode 100644 crates/core/component/auction/src/component/action_handler.rs create mode 100644 crates/core/component/auction/src/component/auction.rs create mode 100644 crates/core/component/auction/src/component/metrics.rs create mode 100644 crates/core/component/auction/src/component/mod.rs create mode 100644 crates/core/component/auction/src/component/rpc.rs create mode 100644 crates/core/component/auction/src/event.rs create mode 100644 crates/core/component/auction/src/genesis.rs create mode 100644 crates/core/component/auction/src/lib.rs create mode 100644 crates/core/component/auction/src/params.rs create mode 100644 crates/core/component/auction/src/state_key.rs diff --git a/Cargo.lock b/Cargo.lock index 7f1e79d1ac..1babc791b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4371,6 +4371,7 @@ dependencies = [ "once_cell", "penumbra-app", "penumbra-asset", + "penumbra-auction", "penumbra-community-pool", "penumbra-compact-block", "penumbra-custody", @@ -4508,6 +4509,7 @@ dependencies = [ "pbjson-types", "penumbra-app", "penumbra-asset", + "penumbra-auction", "penumbra-auto-https", "penumbra-compact-block", "penumbra-custody", @@ -4724,6 +4726,58 @@ dependencies = [ "tracing", ] +[[package]] +name = "penumbra-auction" +version = "0.71.0" +dependencies = [ + "anyhow", + "ark-ff", + "ark-groth16", + "ark-r1cs-std", + "ark-relations", + "ark-serialize", + "ark-snark", + "async-stream", + "async-trait", + "base64 0.21.7", + "bech32", + "bitvec", + "cnidarium", + "cnidarium-component", + "decaf377 0.5.0", + "decaf377-rdsa", + "ed25519-consensus", + "futures", + "hex", + "im", + "metrics", + "once_cell", + "penumbra-asset", + "penumbra-dex", + "penumbra-keys", + "penumbra-num", + "penumbra-proof-params", + "penumbra-proto", + "penumbra-sct", + "penumbra-shielded-pool", + "penumbra-tct", + "penumbra-txhash", + "proptest", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "regex", + "serde", + "serde_unit_struct", + "serde_with", + "sha2 0.10.8", + "tap", + "tendermint", + "tokio", + "tonic", + "tracing", + "tracing-subscriber 0.3.18", +] + [[package]] name = "penumbra-auto-https" version = "0.71.0" @@ -5631,6 +5685,7 @@ dependencies = [ "once_cell", "pbjson-types", "penumbra-asset", + "penumbra-auction", "penumbra-community-pool", "penumbra-dex", "penumbra-fee", diff --git a/Cargo.toml b/Cargo.toml index 2c84b5b206..f840211e34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "crates/core/asset", "crates/core/component/community-pool", "crates/core/component/compact-block", + "crates/core/component/auction", "crates/core/component/dex", "crates/core/component/distributions", "crates/core/component/fee", @@ -163,6 +164,7 @@ penumbra-asset = { default-features = false, path = "crates/co penumbra-community-pool = { default-features = false, path = "crates/core/component/community-pool" } penumbra-compact-block = { default-features = false, path = "crates/core/component/compact-block" } penumbra-custody = { path = "crates/custody" } +penumbra-auction = { default-features = false, path = "crates/core/component/auction" } penumbra-dex = { default-features = false, path = "crates/core/component/dex" } penumbra-distributions = { default-features = false, path = "crates/core/component/distributions" } penumbra-fee = { default-features = false, path = "crates/core/component/fee" } diff --git a/crates/bin/pcli/Cargo.toml b/crates/bin/pcli/Cargo.toml index 415ac3f041..bd12a742d3 100644 --- a/crates/bin/pcli/Cargo.toml +++ b/crates/bin/pcli/Cargo.toml @@ -21,6 +21,7 @@ parallel = [ "penumbra-proof-params/parallel", "decaf377/parallel", "penumbra-shielded-pool/parallel", + "penumbra-auction/parallel", "penumbra-dex/parallel", "penumbra-governance/parallel", "penumbra-stake/parallel", @@ -62,6 +63,7 @@ penumbra-asset = {workspace = true, default-features = false} penumbra-community-pool = {workspace = true, default-features = false} penumbra-compact-block = {workspace = true, default-features = false} penumbra-custody = {workspace = true} +penumbra-auction = {workspace = true, default-features = false} penumbra-dex = {workspace = true, default-features = false} penumbra-fee = {workspace = true, default-features = false} penumbra-governance = {workspace = true, default-features = false} diff --git a/crates/bin/pd/Cargo.toml b/crates/bin/pd/Cargo.toml index b7358d4bf3..161aac8d8b 100644 --- a/crates/bin/pd/Cargo.toml +++ b/crates/bin/pd/Cargo.toml @@ -70,6 +70,7 @@ penumbra-asset = { workspace = true, default-features = true } penumbra-auto-https = { path = "../../util/auto-https" } penumbra-compact-block = { workspace = true, default-features = true } penumbra-custody = { workspace = true } +penumbra-auction = { workspace = true, features = ["parallel"], default-features = true } penumbra-dex = { workspace = true, features = ["parallel"], default-features = true } penumbra-fee = { workspace = true, default-features = true } penumbra-governance = { workspace = true, features = ["parallel"], default-features = true } diff --git a/crates/core/component/auction/Cargo.toml b/crates/core/component/auction/Cargo.toml new file mode 100644 index 0000000000..6b27e31889 --- /dev/null +++ b/crates/core/component/auction/Cargo.toml @@ -0,0 +1,82 @@ +[package] +name = "penumbra-auction" +version = {workspace = true} +edition = {workspace = true} + +[features] +component = [ + "cnidarium-component", + "cnidarium", + "penumbra-sct/component", + "penumbra-shielded-pool/component", + "penumbra-dex/component", + "tokio", + "tonic", + "penumbra-proto/cnidarium", + "penumbra-proto/rpc", + + "futures", + "async-stream", + "async-trait", +] +default = ["component"] +docsrs = [] +parallel = [ + "penumbra-tct/parallel", + "ark-ff/parallel", + "decaf377-rdsa/parallel", + "ark-groth16/parallel", + "ark-r1cs-std/parallel", + "decaf377/parallel", +] + +[dependencies] +anyhow = {workspace = true} +ark-ff = {workspace = true, default-features = false} +ark-groth16 = {workspace = true, default-features = false} +ark-r1cs-std = {workspace = true, default-features = false} +ark-relations = {workspace = true} +ark-serialize = {workspace = true} +ark-snark = {workspace = true} +async-stream = {workspace = true, optional = true} +async-trait = {workspace = true, optional = true} +base64 = {workspace = true} +bech32 = {workspace = true} +bitvec = {workspace = true} +cnidarium = {workspace = true, default-features = false, optional = true} +cnidarium-component = {workspace = true, default-features = false, optional = true} +decaf377 = {workspace = true, features = ["r1cs"], default-features = true} +decaf377-rdsa = {workspace = true} +futures = {workspace = true, optional = true} +hex = {workspace = true} +im = {workspace = true, optional = true} +metrics = {workspace = true} +once_cell = {workspace = true} +penumbra-asset = {workspace = true, default-features = false} +penumbra-keys = {workspace = true, default-features = false} +penumbra-num = {workspace = true, default-features = false} +penumbra-proof-params = {workspace = true, default-features = true} +penumbra-proto = {workspace = true, default-features = true} +penumbra-sct = {workspace = true, default-features = false} +penumbra-dex = {workspace = true, default-features = false} +penumbra-shielded-pool = {workspace = true, default-features = false} +penumbra-tct = {workspace = true, default-features = true} +penumbra-txhash = {workspace = true, default-features = false} +rand_chacha = {workspace = true} +rand_core = {workspace = true} +regex = {workspace = true} +serde = {workspace = true, features = ["derive"]} +serde_unit_struct = {workspace = true} +serde_with = {workspace = true} +sha2 = {workspace = true} +tap = {workspace = true} +tendermint = {workspace = true, default-features = true} +tokio = {workspace = true, features = ["full", "tracing"], optional = true} +tonic = {workspace = true, optional = true} +tracing = {workspace = true} + +[dev-dependencies] +ed25519-consensus = {workspace = true} +proptest = {workspace = true} +rand_chacha = {workspace = true} +tracing-subscriber = {workspace = true} diff --git a/crates/core/component/auction/src/auction.rs b/crates/core/component/auction/src/auction.rs new file mode 100644 index 0000000000..0e58baf59b --- /dev/null +++ b/crates/core/component/auction/src/auction.rs @@ -0,0 +1,2 @@ +pub mod id; +pub mod nft; diff --git a/crates/core/component/auction/src/auction/id.rs b/crates/core/component/auction/src/auction/id.rs new file mode 100644 index 0000000000..d78c1895c7 --- /dev/null +++ b/crates/core/component/auction/src/auction/id.rs @@ -0,0 +1,69 @@ +use anyhow::{bail, Context}; +use penumbra_proto::{ + penumbra::core::component::auction::v1alpha1 as pb, serializers::bech32str, DomainType, +}; +use serde::{Deserialize, Serialize}; + +/// A unique identifier for an auction, obtained from hashing a domain separator +/// and an immutable auction description. +#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Serialize, Deserialize)] +#[serde(try_from = "pb::AuctionId", into = "pb::AuctionId")] +pub struct AuctionId(pub [u8; 32]); + +/* Basic impls */ +impl std::str::FromStr for AuctionId { + type Err = anyhow::Error; + + // IMPORTANT: changing this is state-breaking. + fn from_str(s: &str) -> Result { + let inner = bech32str::decode(s, bech32str::auction_id::BECH32_PREFIX, bech32str::Bech32m)?; + pb::AuctionId { inner }.try_into() + } +} + +impl std::fmt::Debug for AuctionId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + std::fmt::Display::fmt(self, f) + } +} + +impl std::fmt::Display for AuctionId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // IMPORTANT: changing this is state-breaking. + f.write_str(&bech32str::encode( + &self.0, + bech32str::auction_id::BECH32_PREFIX, + bech32str::Bech32m, + )) + } +} + +/* Protobuf impls */ +impl From for pb::AuctionId { + fn from(domain: AuctionId) -> Self { + Self { + inner: domain.0.to_vec(), + } + } +} + +impl DomainType for AuctionId { + type Proto = pb::AuctionId; +} + +impl TryFrom for AuctionId { + type Error = anyhow::Error; + + fn try_from(msg: pb::AuctionId) -> Result { + if msg.inner.is_empty() { + bail!("AuctionId proto message is empty") + } else { + let raw_id: [u8; 32] = msg + .inner + .as_slice() + .try_into() + .context("raw AuctionId must be 32 bytes")?; + Ok(AuctionId(raw_id)) + } + } +} diff --git a/crates/core/component/auction/src/auction/nft.rs b/crates/core/component/auction/src/auction/nft.rs new file mode 100644 index 0000000000..705ee5af3e --- /dev/null +++ b/crates/core/component/auction/src/auction/nft.rs @@ -0,0 +1,41 @@ +use crate::auction::id::AuctionId; +use anyhow::{anyhow, Result}; +use penumbra_proto::{core::component::auction::v1alpha1 as pb, DomainType}; + +/// An non-fungible token (NFT) tracking the state and ownership of an auction. +#[derive(Debug, Clone)] +pub struct AuctionNft { + /// The unique identifier for the auction this nft resolves to. + id: AuctionId, + /// The state of an auction, its specific semantics depend on the + /// type of auction the NFT resolves to. + seq: u64, +} + +/* Protobuf impls ;*/ +impl DomainType for AuctionNft { + type Proto = pb::AuctionNft; +} + +impl From for pb::AuctionNft { + fn from(domain: AuctionNft) -> Self { + Self { + id: Some(domain.id.into()), + seq: domain.seq, + } + } +} + +impl TryFrom for AuctionNft { + type Error = anyhow::Error; + + fn try_from(msg: pb::AuctionNft) -> Result { + Ok(Self { + id: msg + .id + .ok_or_else(|| anyhow!("AuctionNft message is missing an auction id"))? + .try_into()?, + seq: msg.seq, + }) + } +} diff --git a/crates/core/component/auction/src/component/action_handler.rs b/crates/core/component/auction/src/component/action_handler.rs new file mode 100644 index 0000000000..3921930418 --- /dev/null +++ b/crates/core/component/auction/src/component/action_handler.rs @@ -0,0 +1 @@ +// use cnidarium_component::ActionHandler; diff --git a/crates/core/component/auction/src/component/auction.rs b/crates/core/component/auction/src/component/auction.rs new file mode 100644 index 0000000000..e726bba09f --- /dev/null +++ b/crates/core/component/auction/src/component/auction.rs @@ -0,0 +1,57 @@ +use anyhow::Result; +use async_trait::async_trait; +use cnidarium::{StateRead, StateWrite}; +use cnidarium_component::Component; +use std::sync::Arc; +use tendermint::v0_37::abci; +use tracing::instrument; + +pub struct Auction {} +impl Auction {} + +#[async_trait] +impl Component for Auction { + type AppState = (); + + #[instrument(name = "auction", skip(_state, app_state))] + async fn init_chain(_state: S, app_state: Option<&Self::AppState>) { + match app_state { + None => { /* perform upgrade specific check */ } + Some(&()) => {} + } + } + + #[instrument(name = "auction", skip(_state, _begin_block))] + async fn begin_block( + _state: &mut Arc, + _begin_block: &abci::request::BeginBlock, + ) { + } + + #[instrument(name = "auction", skip(_state, _end_block))] + async fn end_block( + _state: &mut Arc, + _end_block: &abci::request::EndBlock, + ) { + } + + #[instrument(name = "auction", skip(_state))] + async fn end_epoch(_state: &mut Arc) -> Result<()> { + Ok(()) + } +} + +/// Extension trait providing read access to auction data. +#[async_trait] +pub trait StateReadExt: StateRead {} + +impl StateReadExt for T {} + +/// Extension trait providing write access to auction data. +#[async_trait] +pub trait StateWriteExt: StateWrite {} + +impl StateWriteExt for T {} + +#[cfg(tests)] +mod tests {} diff --git a/crates/core/component/auction/src/component/metrics.rs b/crates/core/component/auction/src/component/metrics.rs new file mode 100644 index 0000000000..3d73682b5f --- /dev/null +++ b/crates/core/component/auction/src/component/metrics.rs @@ -0,0 +1,25 @@ +//! Crate-specific metrics functionality. +//! +//! This module re-exports the contents of the `metrics` crate. This is +//! effectively a way to monkey-patch the functions in this module into the +//! `metrics` crate, at least from the point of view of the other code in this +//! crate. +//! +//! Code in this crate that wants to use metrics should `use crate::metrics;`, +//! so that this module shadows the `metrics` crate. +//! +//! This trick is probably good to avoid in general, because it could be +//! confusing, but in this limited case, it seems like a clean option. + +pub use metrics::*; + +/// Registers all metrics used by this crate. +pub fn register_metrics() { + // describe_gauge!( + // SOMETHING_TO_MEASURE, + // Unit::Count, + // "A metric that is useful to measure at the resolution of a full node" + // ); +} + +// pub const SOMETHING_TO_MEASURE: &str = "penumbra_auction_something_to_measure"; diff --git a/crates/core/component/auction/src/component/mod.rs b/crates/core/component/auction/src/component/mod.rs new file mode 100644 index 0000000000..c4b55dbc44 --- /dev/null +++ b/crates/core/component/auction/src/component/mod.rs @@ -0,0 +1,6 @@ +pub mod action_handler; +mod auction; +pub mod metrics; +pub mod rpc; + +pub use self::auction::{StateReadExt, StateWriteExt}; diff --git a/crates/core/component/auction/src/component/rpc.rs b/crates/core/component/auction/src/component/rpc.rs new file mode 100644 index 0000000000..6b83abcca8 --- /dev/null +++ b/crates/core/component/auction/src/component/rpc.rs @@ -0,0 +1,25 @@ +#![allow(unused)] // TODO: remove this when filling in the RPCs + +use penumbra_proto::{ + core::component::auction::v1alpha1::query_service_server::QueryService, DomainType, +}; + +use async_stream::try_stream; +use cnidarium::Storage; +use futures::{StreamExt, TryStreamExt}; +use std::pin::Pin; +use tonic::Status; +use tracing::instrument; + +pub struct Server { + storage: Storage, +} + +impl Server { + pub fn new(storage: Storage) -> Self { + Self { storage } + } +} + +#[tonic::async_trait] +impl QueryService for Server {} diff --git a/crates/core/component/auction/src/event.rs b/crates/core/component/auction/src/event.rs new file mode 100644 index 0000000000..8e95357478 --- /dev/null +++ b/crates/core/component/auction/src/event.rs @@ -0,0 +1,5 @@ +use tendermint::abci::{Event, EventAttributeIndexExt}; + +pub fn stub_event(delegate: &Delegate) -> Event { + Event::new("stub_event", []) +} diff --git a/crates/core/component/auction/src/genesis.rs b/crates/core/component/auction/src/genesis.rs new file mode 100644 index 0000000000..77a6f93fee --- /dev/null +++ b/crates/core/component/auction/src/genesis.rs @@ -0,0 +1,37 @@ +use crate::params::AuctionParameters; +use anyhow::Context; +use serde::{Deserialize, Serialize}; + +use penumbra_proto::{penumbra::core::component::auction::v1alpha1 as pb, DomainType}; + +#[derive(Deserialize, Serialize, Debug, Clone, Default)] +#[serde(try_from = "pb::GenesisContent", into = "pb::GenesisContent")] +pub struct Content { + /// The initial configuration parameters for the auction component. + pub auction_params: AuctionParameters, +} + +impl DomainType for Content { + type Proto = pb::GenesisContent; +} + +impl From for pb::GenesisContent { + fn from(value: Content) -> Self { + pb::GenesisContent { + auction_params: Some(value.auction_params.into()), + } + } +} + +impl TryFrom for Content { + type Error = anyhow::Error; + + fn try_from(msg: pb::GenesisContent) -> Result { + Ok(Content { + auction_params: msg + .auction_params + .context("auction params not present in protobuf message")? + .try_into()?, + }) + } +} diff --git a/crates/core/component/auction/src/lib.rs b/crates/core/component/auction/src/lib.rs new file mode 100644 index 0000000000..cf2f70ab1d --- /dev/null +++ b/crates/core/component/auction/src/lib.rs @@ -0,0 +1,13 @@ +// Requires nightly +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![deny(clippy::unwrap_used)] + +pub mod auction; +pub mod params; +pub mod state_key; + +#[cfg(feature = "component")] +pub mod component; + +#[cfg(feature = "component")] +pub use component::{StateReadExt, StateWriteExt}; diff --git a/crates/core/component/auction/src/params.rs b/crates/core/component/auction/src/params.rs new file mode 100644 index 0000000000..b60752e7af --- /dev/null +++ b/crates/core/component/auction/src/params.rs @@ -0,0 +1,25 @@ +use penumbra_proto::core::component::auction::v1alpha1 as pb; +use penumbra_proto::DomainType; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(try_from = "pb::AuctionParameters", into = "pb::AuctionParameters")] +pub struct AuctionParameters {} + +impl DomainType for AuctionParameters { + type Proto = pb::AuctionParameters; +} + +impl From for pb::AuctionParameters { + fn from(_: AuctionParameters) -> Self { + pb::AuctionParameters {} + } +} + +impl TryFrom for AuctionParameters { + type Error = anyhow::Error; + + fn try_from(_: pb::AuctionParameters) -> anyhow::Result { + Ok(AuctionParameters {}) + } +} diff --git a/crates/core/component/auction/src/state_key.rs b/crates/core/component/auction/src/state_key.rs new file mode 100644 index 0000000000..6e7a598c47 --- /dev/null +++ b/crates/core/component/auction/src/state_key.rs @@ -0,0 +1,42 @@ +pub mod parameters { + pub fn key() -> &'static str { + "auction/parameters" + } + + pub fn updated_flag() -> &'static str { + "auction/parameters/updated" + } +} + +pub mod store { + use crate::auction::id::AuctionId; + + pub fn prefix() -> &'static str { + "auction/store/" + } + + pub fn by_id(auction_id: AuctionId) -> String { + format!("{}{auction_id}", prefix()) + } +} + +pub mod dutch { + pub mod trigger { + use crate::auction::id::AuctionId; + + pub fn prefix() -> &'static str { + "auction/dutch/trigger/" + } + + pub fn by_height(trigger_height: u64) -> String { + format!("{}{trigger_height:020}/", prefix()) + } + + pub fn auction_at_height(auction_id: AuctionId, trigger_height: u64) -> String { + format!("{}{auction_id}", by_height(trigger_height)) + } + } +} + +#[cfg(test)] +mod tests {} diff --git a/crates/core/transaction/Cargo.toml b/crates/core/transaction/Cargo.toml index 2f1dae64f2..d5becf8164 100644 --- a/crates/core/transaction/Cargo.toml +++ b/crates/core/transaction/Cargo.toml @@ -9,6 +9,7 @@ std = ["ark-ff/std", "ibc-types/std"] parallel = [ "tokio", "penumbra-shielded-pool/parallel", + "penumbra-auction/parallel", "penumbra-dex/parallel", "penumbra-governance/parallel", "penumbra-stake/parallel", @@ -37,6 +38,7 @@ once_cell = {workspace = true} pbjson-types = {workspace = true} penumbra-asset = {workspace = true, default-features = false} penumbra-community-pool = {workspace = true, default-features = false} +penumbra-auction = {workspace = true, default-features = false} penumbra-dex = {workspace = true, default-features = false} penumbra-fee = {workspace = true, default-features = false} penumbra-governance = {workspace = true, default-features = false} diff --git a/crates/proto/src/serializers/bech32str.rs b/crates/proto/src/serializers/bech32str.rs index 33037efedb..7807aad1fd 100644 --- a/crates/proto/src/serializers/bech32str.rs +++ b/crates/proto/src/serializers/bech32str.rs @@ -270,3 +270,25 @@ pub mod lp_id { serialize_bech32(value, serializer, BECH32_PREFIX, Variant::Bech32m) } } + +pub mod auction_id { + use super::*; + + /// The Bech32 prefix used for Auction IDs. + pub const BECH32_PREFIX: &str = "pauctid"; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + deserialize_bech32(deserializer, BECH32_PREFIX, Variant::Bech32m) + } + + pub fn serialize(value: &T, serializer: S) -> Result + where + S: Serializer, + T: AsRef<[u8]>, + { + serialize_bech32(value, serializer, BECH32_PREFIX, Variant::Bech32m) + } +} diff --git a/deployments/scripts/check-wasm-compat.sh b/deployments/scripts/check-wasm-compat.sh index 26d3088874..9e87e56da6 100755 --- a/deployments/scripts/check-wasm-compat.sh +++ b/deployments/scripts/check-wasm-compat.sh @@ -20,6 +20,7 @@ packages=( penumbra-asset penumbra-community-pool penumbra-compact-block + penumbra-auction penumbra-dex penumbra-distributions penumbra-fee From 2d9c784c5740385fe280432b5f6a6eb40955594b Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Mon, 15 Apr 2024 12:29:11 -0400 Subject: [PATCH 095/168] =?UTF-8?q?fix(asset):=20=F0=9F=94=8A=20`Metadata:?= =?UTF-8?q?=20Debug`=20includes=20all=20fields=20(#4207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes #4190. > The `Debug` impl for `asset::Metadata` throws away all data except for the base denom. This is extremely inconvenient for debugging. this commit updates `Metadata`'s impl of `std::fmt::Debug` so that it includes all of the fields in the `Inner` structure. **note for review:** #4190 didn't explicitly mention which fields to include, so i have included all of them as a starting point. if we should find a middle ground, i am open and interested in hearing about that! #### issue ticket number and link #4190 #### checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > debug representations should not affect consensus. --- crates/core/asset/src/asset/denom_metadata.rs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/core/asset/src/asset/denom_metadata.rs b/crates/core/asset/src/asset/denom_metadata.rs index 82ba6fc893..07006615c1 100644 --- a/crates/core/asset/src/asset/denom_metadata.rs +++ b/crates/core/asset/src/asset/denom_metadata.rs @@ -386,7 +386,28 @@ impl Ord for Metadata { impl Debug for Metadata { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.inner.base_denom.as_str()) + let Self { inner } = self; + let Inner { + id, + base_denom, + description, + images, + units, + display_index, + name, + symbol, + } = inner.as_ref(); + + f.debug_struct("Metadata") + .field("id", id) + .field("base_denom", base_denom) + .field("description", description) + .field("images", images) + .field("units", units) + .field("display_index", display_index) + .field("name", name) + .field("symbol", symbol) + .finish() } } From 2aa709449b89dfea5159a7db46c532f85895a1c8 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Mon, 15 Apr 2024 14:37:48 -0400 Subject: [PATCH 096/168] auction: define main dutch auction messages (#4208) ## Describe your changes This PR adds the main domain types and protobuf definitions for dutch auction support. ## Issue ticket number and link Part of #4206 ## Checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > Component is not hooked to the app yet and this just adds messages/types. --------- Signed-off-by: Erwan Or Co-authored-by: Henry de Valence Co-authored-by: katelyn martin --- crates/core/component/auction/src/auction.rs | 1 + .../component/auction/src/auction/dutch.rs | 183 +++++++ ...enumbra.core.component.auction.v1alpha1.rs | 126 ++++- ...a.core.component.auction.v1alpha1.serde.rs | 517 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 380907 -> 384443 bytes .../component/auction/v1alpha1/auction.proto | 81 ++- 6 files changed, 890 insertions(+), 18 deletions(-) create mode 100644 crates/core/component/auction/src/auction/dutch.rs diff --git a/crates/core/component/auction/src/auction.rs b/crates/core/component/auction/src/auction.rs index 0e58baf59b..8512331f78 100644 --- a/crates/core/component/auction/src/auction.rs +++ b/crates/core/component/auction/src/auction.rs @@ -1,2 +1,3 @@ +pub mod dutch; pub mod id; pub mod nft; diff --git a/crates/core/component/auction/src/auction/dutch.rs b/crates/core/component/auction/src/auction/dutch.rs new file mode 100644 index 0000000000..0f9bb9b4ea --- /dev/null +++ b/crates/core/component/auction/src/auction/dutch.rs @@ -0,0 +1,183 @@ +use anyhow::anyhow; +use penumbra_asset::{asset, Value}; +use penumbra_dex::lp::position::{self}; +use penumbra_num::Amount; +use penumbra_proto::{core::component::auction::v1alpha1 as pb, DomainType}; +use serde::{Deserialize, Serialize}; + +/// A deployed Dutch Auction, containing an immutable description +/// and stateful data about its current state. +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(try_from = "pb::DutchAuction", into = "pb::DutchAuction")] +pub struct DutchAuction { + pub description: DutchAuctionDescription, + pub state: DutchAuctionState, +} + +/* Protobuf impls for `DutchAuction` */ +impl DomainType for DutchAuction { + type Proto = pb::DutchAuction; +} + +impl From for pb::DutchAuction { + fn from(domain: DutchAuction) -> Self { + pb::DutchAuction { + description: Some(domain.description.into()), + state: Some(domain.state.into()), + } + } +} + +impl TryFrom for DutchAuction { + type Error = anyhow::Error; + + fn try_from(msg: pb::DutchAuction) -> Result { + Ok(DutchAuction { + description: msg + .description + .ok_or_else(|| anyhow!("DutchAuction is missing description"))? + .try_into()?, + state: msg + .state + .ok_or_else(|| anyhow!("DutchAuction is missing a state field"))? + .try_into()?, + }) + } +} +/* ********************************** */ + +/// A description of the immutable parts of a dutch auction. +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde( + try_from = "pb::DutchAuctionDescription", + into = "pb::DutchAuctionDescription" +)] +pub struct DutchAuctionDescription { + pub input: Value, + pub output_id: asset::Id, + pub max_output: Amount, + pub min_output: Amount, + pub start_height: u64, + pub end_height: u64, + pub step_count: u64, + pub nonce: [u8; 32], +} + +/* Protobuf impls */ +impl DomainType for DutchAuctionDescription { + type Proto = pb::DutchAuctionDescription; +} + +impl From for pb::DutchAuctionDescription { + fn from(domain: DutchAuctionDescription) -> Self { + Self { + input: Some(domain.input.into()), + output_id: Some(domain.output_id.into()), + max_output: Some(domain.max_output.into()), + min_output: Some(domain.min_output.into()), + start_height: domain.start_height, + end_height: domain.end_height, + step_count: domain.step_count, + nonce: domain.nonce.as_slice().to_vec(), + } + } +} + +impl TryFrom for DutchAuctionDescription { + type Error = anyhow::Error; + + fn try_from(msg: pb::DutchAuctionDescription) -> Result { + let d = DutchAuctionDescription { + input: msg + .input + .ok_or_else(|| anyhow!("DutchAuctionDescription message is missing input"))? + .try_into()?, + output_id: msg + .output_id + .ok_or_else(|| { + anyhow!("DutchAuctionDescription message is missing an output identifier") + })? + .try_into()?, + max_output: msg + .max_output + .ok_or_else(|| anyhow!("DutchAuctionDescription message is missing max output"))? + .try_into()?, + min_output: msg + .min_output + .ok_or_else(|| anyhow!("DutchAuctionDescription message is missing min output"))? + .try_into()?, + start_height: msg.start_height, + end_height: msg.end_height, + step_count: msg.step_count, + nonce: msg.nonce.as_slice().try_into()?, + }; + Ok(d) + } +} +/* ********************************** */ + +/// A stateful description of a dutch auction, recording its state (via a sequence number), +/// the current position id associated to it (if any), and its amount IO. +/// # State +/// We record the state of the dutch auction via an untyped `u64` instead of an enum. +/// This futureproof support for auction types that have a richer state machine e.g. allows +/// claiming a withdrawn auction multiple times, burning and minting a new withdrawn auction +/// with an incremented sequence number. +/// +/// For Dutch auctions: +/// +/// ┌───┐ ┌───┐ ┌───┐ +/// │ 0 │───Closed──▶│ 1 │──Withdrawn─▶│ 2 │ +/// └───┘ └───┘ └───┘ +/// ▲ +/// │ +/// Opened +/// │ +/// +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(try_from = "pb::DutchAuctionState", into = "pb::DutchAuctionState")] +pub struct DutchAuctionState { + pub sequence: u64, + pub current_position: Option, + pub next_trigger: u64, + pub input_reserves: Amount, + pub output_reserves: Amount, +} + +/* Protobuf impls for `DutchAuctionState` */ +impl DomainType for DutchAuctionState { + type Proto = pb::DutchAuctionState; +} + +impl From for pb::DutchAuctionState { + fn from(domain: DutchAuctionState) -> Self { + Self { + seq: domain.sequence, + current_position: domain.current_position.map(Into::into), + next_trigger: domain.next_trigger, + input_reserves: Some(domain.input_reserves.into()), + output_reserves: Some(domain.output_reserves.into()), + } + } +} + +impl TryFrom for DutchAuctionState { + type Error = anyhow::Error; + + fn try_from(msg: pb::DutchAuctionState) -> Result { + Ok(DutchAuctionState { + sequence: msg.seq, + current_position: msg.current_position.map(TryInto::try_into).transpose()?, + next_trigger: msg.next_trigger, + input_reserves: msg + .input_reserves + .ok_or_else(|| anyhow!("DutchAuctionState message is missing input reserves"))? + .try_into()?, + output_reserves: msg + .output_reserves + .ok_or_else(|| anyhow!("DutchAuctionState message is missing output reserves"))? + .try_into()?, + }) + } +} +/* ********************************** */ diff --git a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs index cf2c59a013..49c0e14ccf 100644 --- a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs +++ b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs @@ -1,3 +1,33 @@ +/// The configuration parameters for the auction component. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AuctionParameters {} +impl ::prost::Name for AuctionParameters { + const NAME: &'static str = "AuctionParameters"; + const PACKAGE: &'static str = "penumbra.core.component.auction.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.auction.v1alpha1.{}", Self::NAME + ) + } +} +/// Genesis data for the auction component. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GenesisContent { + /// The configuration parameters for the auction component at genesis. + #[prost(message, optional, tag = "1")] + pub params: ::core::option::Option, +} +impl ::prost::Name for GenesisContent { + const NAME: &'static str = "GenesisContent"; + const PACKAGE: &'static str = "penumbra.core.component.auction.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.auction.v1alpha1.{}", Self::NAME + ) + } +} /// A unique identifier for an auction, obtained from hashing a domain separator /// along with the immutable part of an auction description. #[allow(clippy::derive_partial_eq_without_eq)] @@ -33,12 +63,49 @@ impl ::prost::Name for AuctionNft { ) } } -/// The configuration parameters for the auction component. +/// Describes a Dutch auction using programmatic liquidity on the DEX. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct AuctionParameters {} -impl ::prost::Name for AuctionParameters { - const NAME: &'static str = "AuctionParameters"; +pub struct DutchAuctionDescription { + /// The value the seller wishes to auction. + #[prost(message, optional, tag = "1")] + pub input: ::core::option::Option, + /// The asset ID of the target asset the seller wishes to acquire. + #[prost(message, optional, tag = "2")] + pub output_id: ::core::option::Option, + /// The maximum output the seller can receive. + /// + /// This implicitly defines the starting price for the auction. + #[prost(message, optional, tag = "3")] + pub max_output: ::core::option::Option, + /// The minimum output the seller is willing to receive. + /// + /// This implicitly defines the ending price for the auction. + #[prost(message, optional, tag = "4")] + pub min_output: ::core::option::Option, + /// The block height at which the auction begins. + /// + /// This allows the seller to schedule an auction at a future time. + #[prost(uint64, tag = "5")] + pub start_height: u64, + /// The block height at which the auction ends. + /// + /// Together with `start_height`, `max_output`, and `min_output`, + /// this implicitly defines the speed of the auction. + #[prost(uint64, tag = "6")] + pub end_height: u64, + /// The number of discrete price steps to use for the auction. + /// + /// `end_height - start_height` must be a multiple of `step_count`. + #[prost(uint64, tag = "7")] + pub step_count: u64, + /// A random nonce used to allow identical auctions to have + /// distinct auction IDs. + #[prost(bytes = "vec", tag = "8")] + pub nonce: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for DutchAuctionDescription { + const NAME: &'static str = "DutchAuctionDescription"; const PACKAGE: &'static str = "penumbra.core.component.auction.v1alpha1"; fn full_name() -> ::prost::alloc::string::String { ::prost::alloc::format!( @@ -46,16 +113,55 @@ impl ::prost::Name for AuctionParameters { ) } } -/// Genesis data for the auction component. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct GenesisContent { - /// The configuration parameters for the auction component at genesis. +pub struct DutchAuctionState { + /// The sequence number of the auction state. + /// + /// Dutch auctions move from: + /// 0 (opened) => 1 (closed) => n (withdrawn) + #[prost(uint64, tag = "1")] + pub seq: u64, + /// If present, the current position controlled by this auction. + #[prost(message, optional, tag = "2")] + pub current_position: ::core::option::Option, + /// If present, the next trigger height to step down the price. + #[prost(uint64, tag = "3")] + pub next_trigger: u64, + /// The amount of the input asset directly owned by the auction. + /// + /// The auction may also own the input asset indirectly, + /// via the reserves of `current_position` if it exists. + #[prost(message, optional, tag = "4")] + pub input_reserves: ::core::option::Option, + /// The amount of the output asset directly owned by the auction. + /// + /// The auction may also own the output asset indirectly, + /// via the reserves of `current_position` if it exists. + #[prost(message, optional, tag = "5")] + pub output_reserves: ::core::option::Option, +} +impl ::prost::Name for DutchAuctionState { + const NAME: &'static str = "DutchAuctionState"; + const PACKAGE: &'static str = "penumbra.core.component.auction.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.auction.v1alpha1.{}", Self::NAME + ) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DutchAuction { + /// The immutable data describing the auction and its auction ID. #[prost(message, optional, tag = "1")] - pub params: ::core::option::Option, + pub description: ::core::option::Option, + /// The mutable data describing the auction's execution. + #[prost(message, optional, tag = "2")] + pub state: ::core::option::Option, } -impl ::prost::Name for GenesisContent { - const NAME: &'static str = "GenesisContent"; +impl ::prost::Name for DutchAuction { + const NAME: &'static str = "DutchAuction"; const PACKAGE: &'static str = "penumbra.core.component.auction.v1alpha1"; fn full_name() -> ::prost::alloc::string::String { ::prost::alloc::format!( diff --git a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs index 603d1ccf95..f51a250b0e 100644 --- a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs @@ -283,6 +283,523 @@ impl<'de> serde::Deserialize<'de> for AuctionParameters { deserializer.deserialize_struct("penumbra.core.component.auction.v1alpha1.AuctionParameters", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for DutchAuction { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.description.is_some() { + len += 1; + } + if self.state.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.auction.v1alpha1.DutchAuction", len)?; + if let Some(v) = self.description.as_ref() { + struct_ser.serialize_field("description", v)?; + } + if let Some(v) = self.state.as_ref() { + struct_ser.serialize_field("state", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DutchAuction { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "description", + "state", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Description, + State, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "description" => Ok(GeneratedField::Description), + "state" => Ok(GeneratedField::State), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DutchAuction; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.auction.v1alpha1.DutchAuction") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut description__ = None; + let mut state__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Description => { + if description__.is_some() { + return Err(serde::de::Error::duplicate_field("description")); + } + description__ = map_.next_value()?; + } + GeneratedField::State => { + if state__.is_some() { + return Err(serde::de::Error::duplicate_field("state")); + } + state__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DutchAuction { + description: description__, + state: state__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.auction.v1alpha1.DutchAuction", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for DutchAuctionDescription { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.input.is_some() { + len += 1; + } + if self.output_id.is_some() { + len += 1; + } + if self.max_output.is_some() { + len += 1; + } + if self.min_output.is_some() { + len += 1; + } + if self.start_height != 0 { + len += 1; + } + if self.end_height != 0 { + len += 1; + } + if self.step_count != 0 { + len += 1; + } + if !self.nonce.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.auction.v1alpha1.DutchAuctionDescription", len)?; + if let Some(v) = self.input.as_ref() { + struct_ser.serialize_field("input", v)?; + } + if let Some(v) = self.output_id.as_ref() { + struct_ser.serialize_field("outputId", v)?; + } + if let Some(v) = self.max_output.as_ref() { + struct_ser.serialize_field("maxOutput", v)?; + } + if let Some(v) = self.min_output.as_ref() { + struct_ser.serialize_field("minOutput", v)?; + } + if self.start_height != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("startHeight", ToString::to_string(&self.start_height).as_str())?; + } + if self.end_height != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("endHeight", ToString::to_string(&self.end_height).as_str())?; + } + if self.step_count != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("stepCount", ToString::to_string(&self.step_count).as_str())?; + } + if !self.nonce.is_empty() { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("nonce", pbjson::private::base64::encode(&self.nonce).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DutchAuctionDescription { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "input", + "output_id", + "outputId", + "max_output", + "maxOutput", + "min_output", + "minOutput", + "start_height", + "startHeight", + "end_height", + "endHeight", + "step_count", + "stepCount", + "nonce", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Input, + OutputId, + MaxOutput, + MinOutput, + StartHeight, + EndHeight, + StepCount, + Nonce, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "input" => Ok(GeneratedField::Input), + "outputId" | "output_id" => Ok(GeneratedField::OutputId), + "maxOutput" | "max_output" => Ok(GeneratedField::MaxOutput), + "minOutput" | "min_output" => Ok(GeneratedField::MinOutput), + "startHeight" | "start_height" => Ok(GeneratedField::StartHeight), + "endHeight" | "end_height" => Ok(GeneratedField::EndHeight), + "stepCount" | "step_count" => Ok(GeneratedField::StepCount), + "nonce" => Ok(GeneratedField::Nonce), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DutchAuctionDescription; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.auction.v1alpha1.DutchAuctionDescription") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut input__ = None; + let mut output_id__ = None; + let mut max_output__ = None; + let mut min_output__ = None; + let mut start_height__ = None; + let mut end_height__ = None; + let mut step_count__ = None; + let mut nonce__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Input => { + if input__.is_some() { + return Err(serde::de::Error::duplicate_field("input")); + } + input__ = map_.next_value()?; + } + GeneratedField::OutputId => { + if output_id__.is_some() { + return Err(serde::de::Error::duplicate_field("outputId")); + } + output_id__ = map_.next_value()?; + } + GeneratedField::MaxOutput => { + if max_output__.is_some() { + return Err(serde::de::Error::duplicate_field("maxOutput")); + } + max_output__ = map_.next_value()?; + } + GeneratedField::MinOutput => { + if min_output__.is_some() { + return Err(serde::de::Error::duplicate_field("minOutput")); + } + min_output__ = map_.next_value()?; + } + GeneratedField::StartHeight => { + if start_height__.is_some() { + return Err(serde::de::Error::duplicate_field("startHeight")); + } + start_height__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::EndHeight => { + if end_height__.is_some() { + return Err(serde::de::Error::duplicate_field("endHeight")); + } + end_height__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::StepCount => { + if step_count__.is_some() { + return Err(serde::de::Error::duplicate_field("stepCount")); + } + step_count__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Nonce => { + if nonce__.is_some() { + return Err(serde::de::Error::duplicate_field("nonce")); + } + nonce__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DutchAuctionDescription { + input: input__, + output_id: output_id__, + max_output: max_output__, + min_output: min_output__, + start_height: start_height__.unwrap_or_default(), + end_height: end_height__.unwrap_or_default(), + step_count: step_count__.unwrap_or_default(), + nonce: nonce__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.auction.v1alpha1.DutchAuctionDescription", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for DutchAuctionState { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.seq != 0 { + len += 1; + } + if self.current_position.is_some() { + len += 1; + } + if self.next_trigger != 0 { + len += 1; + } + if self.input_reserves.is_some() { + len += 1; + } + if self.output_reserves.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.auction.v1alpha1.DutchAuctionState", len)?; + if self.seq != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("seq", ToString::to_string(&self.seq).as_str())?; + } + if let Some(v) = self.current_position.as_ref() { + struct_ser.serialize_field("currentPosition", v)?; + } + if self.next_trigger != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("nextTrigger", ToString::to_string(&self.next_trigger).as_str())?; + } + if let Some(v) = self.input_reserves.as_ref() { + struct_ser.serialize_field("inputReserves", v)?; + } + if let Some(v) = self.output_reserves.as_ref() { + struct_ser.serialize_field("outputReserves", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DutchAuctionState { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "seq", + "current_position", + "currentPosition", + "next_trigger", + "nextTrigger", + "input_reserves", + "inputReserves", + "output_reserves", + "outputReserves", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Seq, + CurrentPosition, + NextTrigger, + InputReserves, + OutputReserves, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "seq" => Ok(GeneratedField::Seq), + "currentPosition" | "current_position" => Ok(GeneratedField::CurrentPosition), + "nextTrigger" | "next_trigger" => Ok(GeneratedField::NextTrigger), + "inputReserves" | "input_reserves" => Ok(GeneratedField::InputReserves), + "outputReserves" | "output_reserves" => Ok(GeneratedField::OutputReserves), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DutchAuctionState; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.auction.v1alpha1.DutchAuctionState") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut seq__ = None; + let mut current_position__ = None; + let mut next_trigger__ = None; + let mut input_reserves__ = None; + let mut output_reserves__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Seq => { + if seq__.is_some() { + return Err(serde::de::Error::duplicate_field("seq")); + } + seq__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::CurrentPosition => { + if current_position__.is_some() { + return Err(serde::de::Error::duplicate_field("currentPosition")); + } + current_position__ = map_.next_value()?; + } + GeneratedField::NextTrigger => { + if next_trigger__.is_some() { + return Err(serde::de::Error::duplicate_field("nextTrigger")); + } + next_trigger__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::InputReserves => { + if input_reserves__.is_some() { + return Err(serde::de::Error::duplicate_field("inputReserves")); + } + input_reserves__ = map_.next_value()?; + } + GeneratedField::OutputReserves => { + if output_reserves__.is_some() { + return Err(serde::de::Error::duplicate_field("outputReserves")); + } + output_reserves__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DutchAuctionState { + seq: seq__.unwrap_or_default(), + current_position: current_position__, + next_trigger: next_trigger__.unwrap_or_default(), + input_reserves: input_reserves__, + output_reserves: output_reserves__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.auction.v1alpha1.DutchAuctionState", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for GenesisContent { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 1e88dce399de2ad43e3e5009c5f29ec87633db74..c1d6e1d3ab507b91fdbcc42475b688ea581fb77b 100644 GIT binary patch delta 3465 zcmbVPU1%g(74EuK{ga#DuFX$Rr8Bwd$uJ$sYIbF2jAnKmre{cYjn4G!FwVkmx>9|U zF6pjH>L-~nuq8O)!m=VZQje|=im>dm4}!=D&Vu+L_#lYvi{hgq2qmAYMa+^SLaIdXRy`Qzd&ive6W+q{iUCuuZ%$ciqH(?rR}7b@h3kb=c^+< z+evSi=$IdvUa-^Q_I@X*A zH!csG>6w0mP9`ZEs3H7MetYhPiR3s_to>4y79DLrElov<*M6@q#(yYk<<()m zq4G6{KhST}JhC42qql1Es~3mAl4)v61^(g$9lITJ@8k}`ur2=bHw;?PwOsDC`0(#= zx$=a}PSk0G5-q2zrCilmk0YvGju)v`oXIITE1jViajoduXl_C=R{Gz1XEy!;)yh&w zSD-Umnx(g?rYQtQ#^Z-n+wOn+^}^~nd5(_Jyf}x}__UEpDx`(8^mCLH@I^T{XFT8k z;D<&$J*rh_%B5-gT?H0cht|&%>C6(UgJ~NrDB{P!= zW}$C8`|QAT_dT=MGXvXVUHdS!+jelmT!#f6&Kftq`e}+k6(!JBR$8Q&kQ+?|QzP@n z3VRhwM`kzVQr+j>F8A26?RU5zvB1ro!H5VBA0qG?naU&>t(9jp2@%xJ7|5e=fvaV* zWPDM&Fnyl~Y^%ZCJ?RLREy3dZpV_^Gu5H;t_k^`0zGpkgyM!m0 zEJ-(Jh3&bXJWfk7z(|0xP9{@SA_euSq_xqHz*t9XV~CI)2&=&8B^Yhz6pWE51uz}k z-L7zrYWz32LukRS{~N0ctg8d8qSsdkSXBY946tuXu$r!}5yLo)pET9QDC@snzHdAJ zAa!Q9>mH}sO4h>mP95&iXiwDH?P@UII%dTqAkJaB(D|I!K7&;_MuKTah6k|LX|hP1yoY*xLnM zj7QtXX66CMWQ$f4s}NarG47}J9cv-vohaF{?0Kj^UAZmR3;lrYBK!E&4eSFH0v?c# zY~i*BzzYDrK1hB6z}E-B3jn?j;IB*EMP1z>x^atLV;*wn_Lvii6)h09<>-n|V0Ig8 z*S5@VR(Kgj$2{Ux#E5CK&4J(?q7PVj+p?$_sjHkh0;$AFU|WLBop0C`bJn9`#wzJwj?dxA%F<)>z#=;0|xE zuq!XI7g*iux(G{34yzyAL8tAR$IeQE7OsG{k>(8&U^G%nhXk~Z$!LncB2nS)CRs6F zVq1GCF5U`GuqtUJeTT5*jRNYnTqp2caB-XMo+RCvW-856o(MX;neHP6s_Y z?E0=ymdP8oWP|t;TNvUa+ms4X_EeL-|Ll9vbz02cV|Kv!1N3CpgaD89Zl~-B32|;u zolVm#*l~NQMjt0}f*qS=*?9l7FvZr1!ferH;Qqfr(+!sF%u^#hJ@Cx*(?Q9{H4URo2Pw^}@j11=F<6 zz+`yL_W$(8*lsrHf^6*okD0;Xr>zAu0CJC! z4l{TCKfW>jN^16kk9u+|6#w1~?2C!HpH56cPR#x3%$t~)`_+Zr0-jTW`&s(8GL5f& SM!WpYqfzZAN2B@h7ykrg98b&u delta 431 zcmXw!Jxc>Y5QcYe=RPvGm*h6NoOeWf3k4&Zh}cO`Aw}>XC@HKW(F7!g76GwPkV950 zwGnKr1dCYuH!Q3L5r2Vmn%im~o_Cmc$K&wxHar>ekueg_aA(0y4wn;uIHg}L*w9}# zoaqA_hO4za%67lgJKFB_279}mt!oP{1&l=@I>@CGjHz@ol;CUuuQ(dO6qwA`_a0o@ zj4NR}e)9d;z!o{K0GX$UdDuMWl f)k5TycdY$h_prN&KR#-I4eF=O2ws|z*?;>57+N^q diff --git a/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto b/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto index f325110f25..a605e19fcd 100644 --- a/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto +++ b/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto @@ -1,6 +1,22 @@ syntax = "proto3"; package penumbra.core.component.auction.v1alpha1; +import "penumbra/core/asset/v1/asset.proto"; +import "penumbra/core/component/dex/v1/dex.proto"; +import "penumbra/core/num/v1/num.proto"; + +// The configuration parameters for the auction component. +message AuctionParameters {} + +// Genesis data for the auction component. +message GenesisContent { + // The configuration parameters for the auction component at genesis. + AuctionParameters params = 1; +} + +// Query operations for the Auction component. +service QueryService {} + // A unique identifier for an auction, obtained from hashing a domain separator // along with the immutable part of an auction description. message AuctionId { @@ -13,14 +29,63 @@ message AuctionNft { uint64 seq = 2; } -// The configuration parameters for the auction component. -message AuctionParameters {} +// Describes a Dutch auction using programmatic liquidity on the DEX. +message DutchAuctionDescription { + // The value the seller wishes to auction. + asset.v1.Value input = 1; + // The asset ID of the target asset the seller wishes to acquire. + asset.v1.AssetId output_id = 2; + // The maximum output the seller can receive. + // + // This implicitly defines the starting price for the auction. + num.v1.Amount max_output = 3; + // The minimum output the seller is willing to receive. + // + // This implicitly defines the ending price for the auction. + num.v1.Amount min_output = 4; + // The block height at which the auction begins. + // + // This allows the seller to schedule an auction at a future time. + uint64 start_height = 5; + // The block height at which the auction ends. + // + // Together with `start_height`, `max_output`, and `min_output`, + // this implicitly defines the speed of the auction. + uint64 end_height = 6; + // The number of discrete price steps to use for the auction. + // + // `end_height - start_height` must be a multiple of `step_count`. + uint64 step_count = 7; + // A random nonce used to allow identical auctions to have + // distinct auction IDs. + bytes nonce = 8; +} -// Genesis data for the auction component. -message GenesisContent { - // The configuration parameters for the auction component at genesis. - AuctionParameters params = 1; +message DutchAuctionState { + // The sequence number of the auction state. + // + // Dutch auctions move from: + // 0 (opened) => 1 (closed) => n (withdrawn) + uint64 seq = 1; + // If present, the current position controlled by this auction. + dex.v1.PositionId current_position = 2; + // If present, the next trigger height to step down the price. + uint64 next_trigger = 3; + // The amount of the input asset directly owned by the auction. + // + // The auction may also own the input asset indirectly, + // via the reserves of `current_position` if it exists. + num.v1.Amount input_reserves = 4; + // The amount of the output asset directly owned by the auction. + // + // The auction may also own the output asset indirectly, + // via the reserves of `current_position` if it exists. + num.v1.Amount output_reserves = 5; } -// Query operations for the Auction component. -service QueryService {} +message DutchAuction { + // The immutable data describing the auction and its auction ID. + DutchAuctionDescription description = 1; + // The mutable data describing the auction's execution. + DutchAuctionState state = 2; +} From 333e8db627c35fbbd74ad08391fea6d413a1a610 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Mon, 15 Apr 2024 16:21:11 -0400 Subject: [PATCH 097/168] auction: define schedule action and AH validation scaffold (#4210) ## Describe your changes Create the action definition module and adds the `ActionDutchAuctionSchedule` domain + pb message. ## Issue ticket number and link Part of #4206. ## Checklist before requesting a review - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > Internal and the auction component is not hooked to the rest of the app yet --- .../component/auction/src/auction/dutch.rs | 4 +- .../auction/src/auction/dutch/actions/end.rs | 40 ++++ .../auction/src/auction/dutch/actions/mod.rs | 5 + .../src/auction/dutch/actions/schedule.rs | 41 ++++ .../auction/src/component/action_handler.rs | 2 +- .../src/component/action_handler/dutch/end.rs | 18 ++ .../src/component/action_handler/dutch/mod.rs | 2 + .../action_handler/dutch/schedule.rs | 18 ++ ...enumbra.core.component.auction.v1alpha1.rs | 33 +++ ...a.core.component.auction.v1alpha1.serde.rs | 191 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 384443 -> 384999 bytes .../component/auction/v1alpha1/auction.proto | 11 + 12 files changed, 363 insertions(+), 2 deletions(-) create mode 100644 crates/core/component/auction/src/auction/dutch/actions/end.rs create mode 100644 crates/core/component/auction/src/auction/dutch/actions/mod.rs create mode 100644 crates/core/component/auction/src/auction/dutch/actions/schedule.rs create mode 100644 crates/core/component/auction/src/component/action_handler/dutch/end.rs create mode 100644 crates/core/component/auction/src/component/action_handler/dutch/mod.rs create mode 100644 crates/core/component/auction/src/component/action_handler/dutch/schedule.rs diff --git a/crates/core/component/auction/src/auction/dutch.rs b/crates/core/component/auction/src/auction/dutch.rs index 0f9bb9b4ea..bc83c41aff 100644 --- a/crates/core/component/auction/src/auction/dutch.rs +++ b/crates/core/component/auction/src/auction/dutch.rs @@ -5,6 +5,8 @@ use penumbra_num::Amount; use penumbra_proto::{core::component::auction::v1alpha1 as pb, DomainType}; use serde::{Deserialize, Serialize}; +pub mod actions; + /// A deployed Dutch Auction, containing an immutable description /// and stateful data about its current state. #[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -47,7 +49,7 @@ impl TryFrom for DutchAuction { /* ********************************** */ /// A description of the immutable parts of a dutch auction. -#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)] #[serde( try_from = "pb::DutchAuctionDescription", into = "pb::DutchAuctionDescription" diff --git a/crates/core/component/auction/src/auction/dutch/actions/end.rs b/crates/core/component/auction/src/auction/dutch/actions/end.rs new file mode 100644 index 0000000000..83afb06a27 --- /dev/null +++ b/crates/core/component/auction/src/auction/dutch/actions/end.rs @@ -0,0 +1,40 @@ +use anyhow::anyhow; +use penumbra_proto::{core::component::auction::v1alpha1 as pb, DomainType}; +use serde::{Deserialize, Serialize}; + +use crate::auction::id::AuctionId; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde( + try_from = "pb::ActionDutchAuctionEnd", + into = "pb::ActionDutchAuctionEnd" +)] +pub struct ActionDutchAuctionEnd { + pub auction_id: AuctionId, +} + +/* Protobuf impls */ +impl DomainType for ActionDutchAuctionEnd { + type Proto = pb::ActionDutchAuctionEnd; +} + +impl From for pb::ActionDutchAuctionEnd { + fn from(domain: ActionDutchAuctionEnd) -> Self { + pb::ActionDutchAuctionEnd { + auction_id: Some(domain.auction_id.into()), + } + } +} + +impl TryFrom for ActionDutchAuctionEnd { + type Error = anyhow::Error; + + fn try_from(msg: pb::ActionDutchAuctionEnd) -> Result { + Ok(ActionDutchAuctionEnd { + auction_id: msg + .auction_id + .ok_or_else(|| anyhow!("ActionDutchAuctionEnd message is missing an auction_id"))? + .try_into()?, + }) + } +} diff --git a/crates/core/component/auction/src/auction/dutch/actions/mod.rs b/crates/core/component/auction/src/auction/dutch/actions/mod.rs new file mode 100644 index 0000000000..a783c92e6a --- /dev/null +++ b/crates/core/component/auction/src/auction/dutch/actions/mod.rs @@ -0,0 +1,5 @@ +pub mod schedule; +pub use schedule::ActionDutchAuctionSchedule; + +pub mod end; +pub use end::ActionDutchAuctionEnd; diff --git a/crates/core/component/auction/src/auction/dutch/actions/schedule.rs b/crates/core/component/auction/src/auction/dutch/actions/schedule.rs new file mode 100644 index 0000000000..cb0a11a8bf --- /dev/null +++ b/crates/core/component/auction/src/auction/dutch/actions/schedule.rs @@ -0,0 +1,41 @@ +use crate::auction::dutch::DutchAuctionDescription; +use anyhow::anyhow; +use penumbra_proto::{core::component::auction::v1alpha1 as pb, DomainType}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde( + try_from = "pb::ActionDutchAuctionSchedule", + into = "pb::ActionDutchAuctionSchedule" +)] +pub struct ActionDutchAuctionSchedule { + pub description: DutchAuctionDescription, +} + +/* Protobuf impls */ +impl DomainType for ActionDutchAuctionSchedule { + type Proto = pb::ActionDutchAuctionSchedule; +} + +impl From for pb::ActionDutchAuctionSchedule { + fn from(domain: ActionDutchAuctionSchedule) -> Self { + pb::ActionDutchAuctionSchedule { + description: Some(domain.description.into()), + } + } +} + +impl TryFrom for ActionDutchAuctionSchedule { + type Error = anyhow::Error; + + fn try_from(msg: pb::ActionDutchAuctionSchedule) -> Result { + Ok(ActionDutchAuctionSchedule { + description: msg + .description + .ok_or_else(|| { + anyhow!("ActionDutchAuctionSchedule message is missing a description") + })? + .try_into()?, + }) + } +} diff --git a/crates/core/component/auction/src/component/action_handler.rs b/crates/core/component/auction/src/component/action_handler.rs index 3921930418..83a1901e7b 100644 --- a/crates/core/component/auction/src/component/action_handler.rs +++ b/crates/core/component/auction/src/component/action_handler.rs @@ -1 +1 @@ -// use cnidarium_component::ActionHandler; +pub mod dutch; diff --git a/crates/core/component/auction/src/component/action_handler/dutch/end.rs b/crates/core/component/auction/src/component/action_handler/dutch/end.rs new file mode 100644 index 0000000000..4155a87f5c --- /dev/null +++ b/crates/core/component/auction/src/component/action_handler/dutch/end.rs @@ -0,0 +1,18 @@ +use anyhow::Result; +use async_trait::async_trait; +use cnidarium::StateWrite; +use cnidarium_component::ActionHandler; + +use crate::auction::dutch::actions::ActionDutchAuctionEnd; + +#[async_trait] +impl ActionHandler for ActionDutchAuctionEnd { + type CheckStatelessContext = (); + async fn check_stateless(&self, _context: ()) -> Result<()> { + Ok(()) + } + + async fn check_and_execute(&self, mut _state: S) -> Result<()> { + Ok(()) + } +} diff --git a/crates/core/component/auction/src/component/action_handler/dutch/mod.rs b/crates/core/component/auction/src/component/action_handler/dutch/mod.rs new file mode 100644 index 0000000000..a0320395cf --- /dev/null +++ b/crates/core/component/auction/src/component/action_handler/dutch/mod.rs @@ -0,0 +1,2 @@ +mod end; +mod schedule; diff --git a/crates/core/component/auction/src/component/action_handler/dutch/schedule.rs b/crates/core/component/auction/src/component/action_handler/dutch/schedule.rs new file mode 100644 index 0000000000..fb4e5d60e3 --- /dev/null +++ b/crates/core/component/auction/src/component/action_handler/dutch/schedule.rs @@ -0,0 +1,18 @@ +use anyhow::Result; +use async_trait::async_trait; +use cnidarium::StateWrite; +use cnidarium_component::ActionHandler; + +use crate::auction::dutch::actions::ActionDutchAuctionSchedule; + +#[async_trait] +impl ActionHandler for ActionDutchAuctionSchedule { + type CheckStatelessContext = (); + async fn check_stateless(&self, _context: ()) -> Result<()> { + Ok(()) + } + + async fn check_and_execute(&self, mut _state: S) -> Result<()> { + Ok(()) + } +} diff --git a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs index 49c0e14ccf..9b2da72f93 100644 --- a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs +++ b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs @@ -169,6 +169,39 @@ impl ::prost::Name for DutchAuction { ) } } +/// Initiates a Dutch auction using protocol-controlled liquidity. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ActionDutchAuctionSchedule { + #[prost(message, optional, tag = "1")] + pub description: ::core::option::Option, +} +impl ::prost::Name for ActionDutchAuctionSchedule { + const NAME: &'static str = "ActionDutchAuctionSchedule"; + const PACKAGE: &'static str = "penumbra.core.component.auction.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.auction.v1alpha1.{}", Self::NAME + ) + } +} +/// Terminate the auction associated with the specified `auction_id` +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ActionDutchAuctionEnd { + /// The auction to end. + #[prost(message, optional, tag = "1")] + pub auction_id: ::core::option::Option, +} +impl ::prost::Name for ActionDutchAuctionEnd { + const NAME: &'static str = "ActionDutchAuctionEnd"; + const PACKAGE: &'static str = "penumbra.core.component.auction.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.auction.v1alpha1.{}", Self::NAME + ) + } +} /// Generated client implementations. #[cfg(feature = "rpc")] pub mod query_service_client { diff --git a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs index f51a250b0e..addb620772 100644 --- a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs @@ -1,3 +1,194 @@ +impl serde::Serialize for ActionDutchAuctionEnd { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.auction_id.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.auction.v1alpha1.ActionDutchAuctionEnd", len)?; + if let Some(v) = self.auction_id.as_ref() { + struct_ser.serialize_field("auctionId", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ActionDutchAuctionEnd { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "auction_id", + "auctionId", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + AuctionId, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "auctionId" | "auction_id" => Ok(GeneratedField::AuctionId), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ActionDutchAuctionEnd; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.auction.v1alpha1.ActionDutchAuctionEnd") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut auction_id__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::AuctionId => { + if auction_id__.is_some() { + return Err(serde::de::Error::duplicate_field("auctionId")); + } + auction_id__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(ActionDutchAuctionEnd { + auction_id: auction_id__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.auction.v1alpha1.ActionDutchAuctionEnd", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for ActionDutchAuctionSchedule { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.description.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.auction.v1alpha1.ActionDutchAuctionSchedule", len)?; + if let Some(v) = self.description.as_ref() { + struct_ser.serialize_field("description", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ActionDutchAuctionSchedule { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "description", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Description, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "description" => Ok(GeneratedField::Description), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ActionDutchAuctionSchedule; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.auction.v1alpha1.ActionDutchAuctionSchedule") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut description__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Description => { + if description__.is_some() { + return Err(serde::de::Error::duplicate_field("description")); + } + description__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(ActionDutchAuctionSchedule { + description: description__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.auction.v1alpha1.ActionDutchAuctionSchedule", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for AuctionId { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index c1d6e1d3ab507b91fdbcc42475b688ea581fb77b..6ff75478eb71caed17c307b47814f7dd67ede248 100644 GIT binary patch delta 482 zcmZXQ%SyvQ6o#8*+Q!qMG*D}zVl0SOP*71Ni!oIjG3BFl;T2#f)5~) ztt+2E=t9wr?;^MopTJ3LapA1yKezuo=lMDN@s_=ANiDghJfVjKy3fPXUHWpU{gOa) z@v+>E1m>EjwaKx1&t)-RkRjSWwaM zXy0#e3(xn1wjU6`c}VX7Z%k?`vg8Er#DpV}bofQ*tlD#pr^xoT~BlvA6Bdjn|X ziZh3~GY<2(&s>*a%cW-l#gsQ^K|BQ|C0!}1IY(a^s1t`CQxU5C zEnn#}sluwNjzUf_sSy*Fhs}(2)ScNG$(cnwEOv-cMU6nka;m39KNfemg!t@GSzAYH2T`=Oo`wxt@c{cz6 delta 85 zcmV-b0IL7zzZbi^7l4ETgaU*Ev;?3A1nDFSHj@#17`MCz1Th5~G7bt1QFUc#|(!r8w9s68wI$;s4E(e diff --git a/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto b/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto index a605e19fcd..1afde18708 100644 --- a/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto +++ b/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto @@ -89,3 +89,14 @@ message DutchAuction { // The mutable data describing the auction's execution. DutchAuctionState state = 2; } + +// Initiates a Dutch auction using protocol-controlled liquidity. +message ActionDutchAuctionSchedule { + DutchAuctionDescription description = 1; +} + +// Terminate the auction associated with the specified `auction_id` +message ActionDutchAuctionEnd { + // The auction to end. + AuctionId auction_id = 1; +} From b459268ba628f1e66ff0e7487779ef96520faad1 Mon Sep 17 00:00:00 2001 From: Henry de Valence Date: Mon, 15 Apr 2024 13:57:35 -0700 Subject: [PATCH 098/168] docs: restructure guide TOC and landing page --- docs/guide/book.toml | 2 +- docs/guide/src/README.md | 156 +++++++----------- docs/guide/src/SUMMARY.md | 27 +-- .../src/assets/interchain-shielded-pool.jpg | Bin 0 -> 998891 bytes docs/guide/src/dev/devnet-quickstart.md | 2 +- docs/guide/src/dev/rpc.md | 8 +- docs/guide/src/node.md | 16 ++ docs/guide/src/{ => node}/pclientd.md | 0 .../{ => node}/pclientd/build_transaction.md | 0 .../src/{ => node}/pclientd/configure.md | 2 +- docs/guide/src/{ => node}/pclientd/rpc.md | 0 docs/guide/src/{ => node}/pd.md | 2 +- docs/guide/src/{ => node}/pd/chain-upgrade.md | 0 docs/guide/src/{ => node}/pd/debugging.md | 0 docs/guide/src/{ => node}/pd/install.md | 2 +- docs/guide/src/{ => node}/pd/join-testnet.md | 0 docs/guide/src/{ => node}/pd/requirements.md | 2 +- docs/guide/src/{ => node}/pd/validator.md | 4 +- docs/guide/src/{extension.md => web.md} | 0 19 files changed, 104 insertions(+), 119 deletions(-) create mode 100644 docs/guide/src/assets/interchain-shielded-pool.jpg create mode 100644 docs/guide/src/node.md rename docs/guide/src/{ => node}/pclientd.md (100%) rename docs/guide/src/{ => node}/pclientd/build_transaction.md (100%) rename docs/guide/src/{ => node}/pclientd/configure.md (97%) rename docs/guide/src/{ => node}/pclientd/rpc.md (100%) rename docs/guide/src/{ => node}/pd.md (66%) rename docs/guide/src/{ => node}/pd/chain-upgrade.md (100%) rename docs/guide/src/{ => node}/pd/debugging.md (100%) rename docs/guide/src/{ => node}/pd/install.md (95%) rename docs/guide/src/{ => node}/pd/join-testnet.md (100%) rename docs/guide/src/{ => node}/pd/requirements.md (94%) rename docs/guide/src/{ => node}/pd/validator.md (97%) rename docs/guide/src/{extension.md => web.md} (100%) diff --git a/docs/guide/book.toml b/docs/guide/book.toml index 3557436510..84f29b408a 100644 --- a/docs/guide/book.toml +++ b/docs/guide/book.toml @@ -15,7 +15,7 @@ command = "mdbook-mermaid" default-theme = "coal" preferred-dark-theme = "coal" curly-quotes = true -fold = { enable = true, level = 1 } +fold = { enable = true, level = 2 } git-repository-url = "https://github.com/penumbra-zone/penumbra" additional-js =["theme/js/mermaid.min.js", "theme/js/mermaid-init.js"] diff --git a/docs/guide/src/README.md b/docs/guide/src/README.md index 18e889da92..1d77e5aaec 100644 --- a/docs/guide/src/README.md +++ b/docs/guide/src/README.md @@ -1,99 +1,67 @@ # Penumbra Guide -[Penumbra] is a fully shielded zone for the Cosmos ecosystem, allowing anyone to -securely transact, stake, swap, or marketmake without broadcasting their -personal information to the world. - -This site contains documentation on how to use, deploy, and develop the Penumbra -software. The description of the protocol itself can be found in the [protocol -specification][protocol], and the API documentation can be found -[here][rustdoc]. - -## Test networks - -Penumbra is a decentralized protocol, so Penumbra Labs is [building in -public][how-were-building], launching (and crashing) lots of work-in-progress -testnets to allow community participation, engagement, and feedback. - -Currently, Penumbra only has a command line client, `pcli` (pronounced -"pickle-y"), which bundles all of the client components in one binary, and a -chain-scanning daemon, `pclientd`, which runs just the view service, without spend -capability. To get started with the Penumbra test network, all that's required -is to download and build `pcli`, as described in -[Installation](./pcli/install.md). - -The Penumbra node software is the Penumbra daemon, `pd`. This is an ABCI -application, which must be driven by CometBFT, so a Penumbra full node -consists of both a `pd` instance and a `cometbft` instance. - -The basic architecture of Penumbra is as follows: - -```text - ╭ ┌───────┐ - spending│ │custody│ -capability│ │service│ - ╰ └───────┘ - ▲ │ - │tx │auth - │plan │data - │ ▼ - ╭ ┌───────┐ - viewing│ │wallet │ tx submission -capability│ │logic │────────┐ - │ └───────┘ │ - │ ▲ │ - │ │view private state - │ │ │ - │ │ │ - │ ┌───────┐ │ - │ │view │ │ - │ │service│ │ - ╰ └───────┘ │ - ▲ │ - │sync private state - │ │ - ╭ ┌──┼───────────────┼──────┐ - public│ │ │ Penumbra Fullnode│ - chain│ │ │ │ │ - data│ │ │ ▼ │ - │ │ ┌──┐ app ┌──────────┐ │ - │ │ │pd│◀─────▶│ cometbft │ │ - │ │ └──┘ sync └──────────┘ │ - │ │ ▲ │ - ╰ └───────────────┼─────────┘ - .──│. - ,' │ `. - .───; │consensus - ; │sync - .─┤ │ ├──. - ,' │ `. - ; Penumbra │ : - : Network ◀──┘ ; - ╲ ╱ - `. `. `. ,' - `───' `───' `───' -``` - -The custody service holds signing keys and is responsible for authorizing -transaction plans. The view service holds viewing keys and scans the chain -state. Wallet logic can query the view service to get information about what -funds are available, submit a transaction plan to the custody service for -signing, and then use the returned signatures to build the transaction and -submit it. - -As a shielded chain, Penumbra's architecture is slightly different than a -transparent chain, because user data such as account balances, transaction -activity, etc., is not part of the public chain state. This means that clients -need to synchronize with the chain to build a copy of the private user data they -have access to. This logic is provided by the *view service*, which is bundled -into `pcli`, but can also be run as a standalone `pclientd` daemon. - -Modeling authorization as an (asynchronous) RPC to a custody service means that -the client software is compatible with many different custody flows by default --- an in-process "SoftHSM", a hardware wallet with user intervention, a cluster -of online threshold signers, an offline threshold signing process, etc. - -[how-were-building]: https://penumbra.zone/blog/how-were-building-penumbra +[Penumbra] is a fully private, cross-chain proof-of-stake network and +decentralized exchange for the Cosmos and beyond. Penumbra brings privacy to +IBC, allowing anyone to shield any IBC asset just by transferring it into +Penumbra's multi-asset shielded pool. Within Penumbra, users can transact, +stake, swap, or marketmake without broadcasting their personal information to +the world. + + + + + +Unlike a transparent chain, where all information is public, Penumbra is +end-to-end encrypted. + +## Using Penumbra on the web + +The easiest way to get started using Penumbra is with the Penumbra web +extension. The web extension runs entirely locally, and contains an embedded +ultralight node that syncs and decrypts only the data visible to your wallet. +Websites can request to connect to your wallet and query your data. + +The [_Using Penumbra on the web_](./web.md) chapter describes how to use +Penumbra on the web. + +## Using Penumbra from the command line + +Penumbra also has a command-line client, `pcli`. Some protocol features, such +as threshold custody for shielded multisigs, do not yet have support in web +frontends and are only accessible via the command line. + +The [_Using Penumbra from the command line_](./pcli.md) chapter describes how to +use `pcli`. + +## Running a node + +Running a node is not necessary to use the protocol. Both the web extension and +`pcli` are designed to operate with any RPC endpoint. However, we've tried to +make it as easy as possible to run nodes so that users can host their own RPC. + +There are two kinds of Penumbra nodes: + +* Penumbra fullnodes run `pd` and `cometbft` to synchronize and verify the entire chain state, as described in [_Running a node: `pd`_](./node/pd.md). +* Penumbra ultralight nodes run `pclientd` to scan, decrypt, and synchronize a specific wallet's data, as well as build and sign transactions, as described in [_Running a node: `pclientd`_](./node/pclientd.md). + +The web extension and `pcli` embed the view and custody functionality provided +by `pclientd`, so it is not necessary to run `pclientd` to use them. Instead, +`pclientd` is intended to act as a local RPC for programmatic tooling (e.g., +trading bots) not written in Rust that cannot easily embed the code for working +with Penumbra's shielded cryptography. + +## Participating in development + +Penumbra is a decentralized, open-source protocol, built in public. + +The [_Participating in development_](./dev.md) chapter has developer +documentation for working on the protocol itself. + +## Resources + +The [_Resources_](./resources.md) chapter has links to other resources about the Penumbra protocol. + + [protocol]: https://protocol.penumbra.zone [rustdoc]: https://rustdoc.penumbra.zone [Penumbra]: https://github.com/penumbra-zone/penumbra diff --git a/docs/guide/src/SUMMARY.md b/docs/guide/src/SUMMARY.md index dc9fce7f5d..7b1235e3fb 100644 --- a/docs/guide/src/SUMMARY.md +++ b/docs/guide/src/SUMMARY.md @@ -1,23 +1,24 @@ [Penumbra Guide](./README.md) -- [Using the web wallet](./extension.md) -- [Using `pcli`](./pcli.md) +- [Using Penumbra on the web](./web.md) +- [Using Penumbra from the command line](./pcli.md) - [Installing `pcli`](./pcli/install.md) - [Generating a Wallet](./pcli/wallet.md) - [Updating to the latest testnet](./pcli/update.md) - [Viewing Balances](./pcli/balance.md) - [Sending Transactions](./pcli/transaction.md) - [Using Governance](./pcli/governance.md) -- [Running a `pd` fullnode](./pd.md) - - [Requirements](./pd/requirements.md) - - [Installing `pd`](./pd/install.md) - - [Joining a testnet](./pd/join-testnet.md) - - [Becoming a validator](./pd/validator.md) - - [Performing a chain upgrade](./pd/chain-upgrade.md) - - [Debugging](./pd/debugging.md) -- [Local RPC with `pclientd`](./pclientd.md) - - [Configuring `pclientd`](./pclientd/configure.md) - - [Making RPC requests](./pclientd/rpc.md) - - [Building Transactions](./pclientd/build_transaction.md) +- [Running a node](./node.md) + - [Full node: `pd`](./node/pd.md) + - [Requirements](./node/pd/requirements.md) + - [Installing `pd`](./node/pd/install.md) + - [Joining a testnet](./node/pd/join-testnet.md) + - [Becoming a validator](./node/pd/validator.md) + - [Performing a chain upgrade](./node/pd/chain-upgrade.md) + - [Debugging](./node/pd/debugging.md) + - [Ultralight node: `pclientd`](./node/pclientd.md) + - [Configuring `pclientd`](./node/pclientd/configure.md) + - [Making RPC requests](./node/pclientd/rpc.md) + - [Building Transactions](./node/pclientd/build_transaction.md) - [Development](./dev.md) - [Compiling from source](./dev/build.md) - [Devnet Quickstart](./dev/devnet-quickstart.md) diff --git a/docs/guide/src/assets/interchain-shielded-pool.jpg b/docs/guide/src/assets/interchain-shielded-pool.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e7715adef13aeca4c545b73e51f1b1885bdf4f53 GIT binary patch literal 998891 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 z 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μ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

or^NVk9R;JL#gzZK6DbZ&E)- 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%0AgrXy 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

=l6?{&6lb2}m$8#0Q=|B_m zV;R|86Qxs_v{SN^#N^$OqH4Qn+06Jd)+V{Gx%B?$$7aU zB7NtZ!xJF|W}phcFpHv?aFLJvxh-aB`1{IBWrYK!nD?Os)qM3F;ki5SX!@+Dif5E& zVBWUU(^q27*d?blC%PN1{U3~oC-FOMaa5YE&3owju3{faR}xsRr&g%z+p}3!%N=7qzFF6E#uaeHpNes|wt; zPxi4`A7i9iu9T}USX(%7kn>XvP!(Sg8HRj|cG`W~$QC#|NiIzP>;Y{#m zN<|*3N4VrE&Ubm?9%t@ZGZKVWxo6>szq5&wQBkmolFynM_BkUc5~!e0R7CjVE|3h2@(b6s4W# zaa5+4R2Swl>%}O|jgJpAVn^IJwX99tMWmx<(f3;Ebm zhGKX)jd+G15iE3n5Ng&J%(;JzKi)b~H{#M(rr6%LZJAqGy{xkAZ5zs;^KZNO87KnXy^yRL{8j3a~?YYosiFL&pJ?+nUTA=!gjFG+8#+ zwj1KHp>pQ>L(a((M`!jQ|G^}Lg>Dvu9J;ban{B1+gKY8y$^LY{FOCs)Nm$?b{DT>> z!#G{hy$s*c7TJ1UuU3dS&md@uL?*GxCtE|7WOgxWEUCB@S*14^;o)8|nmE%%`Uf-q znJVfwN*zKrD<{=CI{8TV*SdJo1o97tN9E}s3`o{rwwTlPUO-!IG$a(dC~u`R_e5bT zvZL2EhppMTTx(GUY}55I69H{Cx+O?7rOn?Z;OaCP^`V1EMW0~e4*8)&tQDPcVT@H>xXAc2jsy z)|O^HhM@T4T?Xl4vsajU9XIMv%TrILzvr2KT@(2r<#E)b{s+IYBv3_Zxw}pJe$qRw z!6-jkalI`)DYDge4DJ>DM!^gH=kf1VHJhM(X~gfYkB-X9|MAOO&>iDo^C-?bbK&|B z# zgzro=RqkTML!@~vVI%?mGSQ-Vbqj+A-@xU*V@ZaR^S8aUfnSUNyJyaY z*1xFzr)gIyV|{6xI-H*!<`4P54A1{b!N30Q6r%Oze9Apct%UDK2$bIb0O<N}aeDxuZo{huy%qg~+0n7# z22(mKDc5nwz#r|4sE>R_mg(@p0=4+5uFm}@O^4T#hQhP7;SBx%orX4Bl~0h?>=7vp zaY;IgE_7ELUUIQ|~#erDl#DZg7RwrTd-{*Cu3 zKfy9Yf@t4qf5y48j2nPmTDxu!^cr=3<$cBJXxIOUvavHy5q$uhfV$m5z|NxnQ|6Dm zG}+4BQ0({%ZVe=gwcaai33|snuN1rt-RbgVaD;j@;jp9aQNO(<56d$qiU`g^#oR7X zF*jOUfh{`^H9R){Oi#xNR4-przDlf6ytBX@U6Lfu(r*J&Mo{$0-aTq*9SWz}uTcIU zE8nKaUswIy$ytY%SG@WDc{kMza8R2yBg&6wA@ZO9x?ewTWt`@ZA!c=ZC`gYWz@kVv zdF55)Ig93bFDQuR+P#z$EUwNxGRqFkmP)5_2}hld(J5jc)f_Wri@@B34DGy>cSzZs zQm?epl^MlQbhrpeadlIVs+-~m%Re@%Xx&(k3L`^x{0r#f++B(wVa4)imK8nWh&G+J z(aOCQi(j^DKD^ZxZyXXZ4f5ksVlUNyyl6_TLh8Qyc!4`p41a7sR>!`{JY;`e1t0MK zl{;#x>XI*vJAoo?oA&u5(NKw%jHPbwjEnB(9rRFPi@8NDOYPHc-grU1F69IMKVJLl zj_xk{RTRy%Y0p)AKASDo-|q2>1G#w-QxE6YWO3^6CIJ0QlGSs&fb2G5|5HFlip=fi z+^xo1zPo!J1#WHYA4Fa>Z%|8rOdQq-@l4t9*_Fz65)aG(wP$>1G3O!8OY%cY+Z4!O zv1aFGZ9#-iwTbQ)X0UH+w{0U_f(_(^&5Dd{4liEpOCz@iwuoAnk->i|aKGC6eI6Wt z)WJC^oH2NU3oKV~<*sg(#-8I_FSkM*wfEam?u zmXBb6J980`d@olxh5BPTUWTbtI}0OOb>F_}emjV7RK_qxWWDcIjzY^yMT;7qskXJj zh43I(l$J$0DLf+>m%|TrB35_gb_w-3-em-Y>2L_V?u+;8s@SHmPzFWGg?3)PF;^Ka zhmciNfEeL4L%6}qipN1Yg=t}r&g0W3l1kfLu(sv4@iS6*nQ#o!){Y<7T?rXZ$X6g- z15+Kh&UZP+kQuwTvVx)#hxDBx_ak#2*c9Qh$FF(=q?lpj4{bEEyN4-3=iD06%cmvX zoQ+P<_QaxfsKS3H%ubln!~Uc4kPjkPGv(lEv7SO(6K$P$S*DZ~NM-v!&$`&hLR07Lp!kIde;CeXbSdNp zZ@yYWSL@2u6iC#A_^sL}`>bWXLP@Vn#yeC(Bx49ssZMr$o`_--9+y?b786KPy`!P^+CY#H$R9>6*3 z8NXf|-KQBcmo-gVwsZas8r~pM+4&;p@|231YoNJ+@@#5~#ALTFMS|CoO~xEE4YIV4XV*RjUlo|0x8P+IA=R4uf=U~ITp2RCtDe!!p4R_b zFz0CZmZ_wAx&LKyaieyu1+aZh`AlWZt4eJvMEX#r2im`3sSf3G1UWez~ZizQT32nE9kwZl_BJb%SRH z;{>gO7-OYFggJShE#UamTw8UYzu4gJ26z~K)*+u3pya3!0^h4x0j{;J1`H^aaf!ryS`V6Bx?PD9 z)Rl&6BJ{?=rLu)g<0D7)72dx}*{n~mWr_(wSv#n$XxuO!@PvCqOQSh~ChTS>4Zix! zbLWDMu3hF|>IW3gwOr0vy!3c7I@+UGCcvo?IpJKlKL^by8N^K=oRv3k@U;mVz;n$N z^_#a|i#l{a3$0PL2Q^#U*a{dlu=vJpwWUAu95xM`x-y!!qYLFreMshwL2~pQh4Xv4+s<_$!fy~a?C`9d<~zsOr{Yc6^=ToW z44x`Y>)#-=|UpVFOCZ_hwGSF*pkXDys>XsR6O{l_IZ8l{F3^l3V8D0D$x=NDjISFB@u?=`*DM z$50cyP*5RSrF4rpf$PtMLc0D91Byb{w5-J$(oD=Lf9?>Ph|cvwuxtda$(Je=GJTx- zC%1nvBwuup&S|{RG~!ECic}YXGO0h|3#gh|lst1|kG=xky;>L95gaB*^l5KW35e1J z8laWx3t_6yowC3UQxEIVaz*%WGxLi-=HLbEFkm4yS-ie+R`wUhCg)y;-y5bBp4=QE z*=WQ#S;7-0i2)mrrseC1;D|Obw@Sm$#R*MpEk7EB)IgyoF$pj-#>-$jntnPCcXXwOlr|1q&462V z-UxIJkjy{!b4a#uHkb=?@iT{Ft=gdQOG!z6|HPl!WJJ5-ruSA~Rcc-kg9PLx{~+Ia z{O2ua1+-|tiXt{JIV-jH9RX-?MxT|KlC1%Zc#W@*)Y;&=q*emQ>{>_)>$Jked{>a= zDUm4n0!m-Nl4guej`4t!*wd%shQdJ~@&ayv=Z^veuunYdY|DdFbFpmOCpOLCWO}&i zEiQ!~+OW8^TDx>knw+Ja=@-5?Y<^_~t-9p$Ve%~AdXtpinahCc zs4fM%nfbuF+hJ2WRj8|uHYVI^Wn*hobe_9h=HDOIn6p^$AEVQse~tHu2Sa3jo$92v zWR2YmLIq{kfDj6qX@fm&jE>)Jh|TfMhplqrcwex(cs-ouILEg%+uqb&_}x@FFqY}4 ze!16iy^dufsQ;N&Rvn`}tN>&(!WdC)j%9=T>20JpM|bB=wj-6cH7bqm|hu zf$l%A5L!dAC#=c_w^Q`SLnMRxv3RTe@PrCmk5YfG+ke*tRdXA!4+@vqg&ne6OQbfZj8-TKe;|b9>&*^g4gG6n3MxOqM=fZ^w#i?hSm$37ypCNxS%RW%>>&3 z=(d#k?9j-dLtY0|Ok6$wh^p}_PVB~LUjqJ9`N!(0{ha#Zf@F+x%3C|s=%#fc#&+T8 zJJB&m|E=(6Nu22BFD!jd&^2?1!@0zsJKFq%qB1v^?i6ks+2LithB3^mZ@tK1&(#`TG1`(GCU2y=r8u3hbxDXD8_|<5(BNXp5y=b1Py(|x z1si)h5E@H(3?hnRbjnd&kh4Bb_SjY9U=^o$Ylf(SsT9b05gVGeulR5D7l}fensEyX zqhnni;wyc!ik1DOuaGAS`Vr_Fa8yDXM2U3<*Mf9=C|lZpNz4kMhj^ES3=~~B$H3ZJ zAj6dvCe<_L*z?rLV^5*!n+@_^pqk2NdcnFaswf!;S_Jm&x-!w6{@7M=&kv4oXs$ON zO0aTiJw-tSm1g|L|L|5te+Z0;MKh*7D-_CE_*Uduiq(>ss_3mb=zClXY>reOUp03S zix@uq>`BZo*fgyc3c9=HlX1+%XaXS(}0175=v#=y!$IKXixA@W!%2wi)u@$5PvLo zc5`vFdbriKOkmWJ;^%2hNGwOR*aL<2oaj8~!zaE_7o;1^Sp4a-9#aq-ce&OkT&2y; zmdMaD$gb+VA|lfV-M5rDS?Yy0?fj%A}G+mjg~Wiis}0{WQ7BdH~UljVs0 z<`rnZIIxu;CT%T4olj3#PdjX~`DKpf1y}s5@-Fd2I!)a`sZsGoZLgalg7MiM&Cu>g z+MyZYRMH{+o=F%}K6GjQU8U~NAq3P7=w)Ft!Y!a~P`z~LwO zh0fYw2tz;vk9)sRxdWKBGgCPuXMaE8Ick{IgBC}KS^bySmA?#l3$9UpDUVQoU`GL-Z69j0EW57lJVDrX?`G%+Q z#-#WbDYcK+AtZ1H99o_Rn*-xVSAXQ!>%Zv?M}D51jROy z&!-KGe3cDMLy=lX9Vc3^vqE%S$+gq60**J!XIYlRiy2{|8xF_C;Bey%DHQpky;!s= z0N6BZm0R_x>*Q6Bf5q`Wu_<}!*Cna^W2n9c#wbKAfnB%D4(ZevhmYN5DlbFWf6;xF zvXIF=5w)_9zNjRIjfl{-rj|1P{W`xiFyBU6ms;5+j0Z(9pw-otNc_-|J@Jcib#6^9 zVl$#%$w9Aa1Y~XFrxPP-UZW=>HV6kK0% zF_Dfmx%;W||*Pq7~g%^!b!e0^*lEa|kdip$j z&9!TE-CLb9tpVc2vyEeip%_*T7SBbfW>JXa&I+{3lfwUb*t8Or$I1PV;e|j#MFbF7 zr70b%?p`5XQp%?f5k&7gLlH+T1NZnuS{BjF%uAI!Oy3C&sbkI{Q6%WPkD!h{1`MQf zaBx(VHx|!T+}mZRPvdrJ2=fhQltf~0ZP&Y2l?EqVyC_KGdz0E|z#~vZ)Lx}rmJK#1 zSZ6X3jJa3<-L;Ep}k|qGA;bOMPJd&Mo1e1wC1rB&T#AYj}R!@ z9Acm+^02INn!cU!i%v=$I~Jo1b&*fOhSZmhn#zQnol2~XQ-D>pX39B)Aj7prS{`e9!_P%fT* zP7!^M3cKZ_0_;FpW(izx4kdgt>Mg4q;dL|1-)F^+b5((=1$2g26vGlTPrh7|#>P6= zBt%;B%qnVXnYmq@jRB8J%#EoHre@wO(=V%!R|S{e>n>7(twSl+C*fzL^u*FtU_FYm z;#f8wd&rC584`iM&egF=X^epaCdQWcxwU&+L=(3XeOY)UnCm%KHFZ^y)_}uch@XO{cML$Z^%%!+jyE?!+Pt!~-4&Ic$R}BD5?F6;1{Xirn2ZAXd#K@f&dO*V^^A6sT;f(;-2}30KqTzmSt}HKZS~YI554OgcF0 zRHN=OTMHIGdP`uOWHh>sPtcqqjY+JsCpZjWEAOrnt5mROE!YDNTJ1GL&c+go-EGof z_c)?sJ+&s!=Zh--Dv-j$2dk^AO*h7rreDNAByx=G+$fE$ze*emiE}nP&S>?4g>(;0 z?HAc6kG+#~l{AIwII=*=M_U4_AwarY{xV@x{EFG}JV@o@xSJ?;|8 zet0@FXz*x_(8N6t?AQl>3oW=&S9h!L;P?2cw<5rV_=~u2{wb=FT^KHvxrzkCVs*Sf z>?=TYzV&5As;nZ#f6xQhAFx6dW1Ju$>5pchDteeIESAcGoKHk9P}}BIk7#{ytTC?H zu>BNWKu*GUG9a}fue=h%b1@{~D+8R`heWah1}a>LU_nT6mqb60_zt37LQ1mQeCFG9 zhE%?vEF+k%SUb)a$z!_jo#~kRtrMf({eyx1QQ^w$2{5CMn${w}vDT$Nz-$Qg(b(5) zs2Ng{c0qB9m40qOHvi&STdeJJ;Yswe(d*zEe-u9P=d&-^eeHS)_z7oDbo&~?o!8og zi?U4BQBL)^&zjoBhR%#k7rDAbMPNZ*NH#zD-aLxk@O03C+L$vz?;vj9!+PKOLVxyF zda=sxXNR?=vgWVL6W?3dfGkx^zm50WYzoyk60Je*>9(GY$pin0rcg{!F^Ee^?kSSj zocjv78{D*qUe=4kTf~v2n#wS4*JR>ViD~O4vL=GyRV5S9Cd@2jpTZ+gsn>eZ$* zo3||7way~BM?2S%kV>=!(VP1gV1V~cV~aXIGPe^c z%D+|l*gQh=Lfo4L|8({s!+7eO%VX1LS%~T`>_13FoQPQFhj2H^fRL|UvcADExqW$} zrbN#%$=8f8bX|>f?WQJ)E~wg^YpS^0P$UnJ0Z^FQAWb6;iA?BxHV&QRF1Mn&^{n(f zp`4W#%IpL&Jj8^=u#aOQsI-SXpHI9Y(kHvK@zNJUTY}&waGX#+e zq5L(gdYw71#R;xL70z`XlL1exFU~2#t{~A`BnR zef3|Bt!uPz+^W*}>3OvVkA&HI01n&)!bP4Q#&kTps_@=z47;iI*x!SeF)$?3`$4FX zYH|yzY#0V*+1GN{+clagylsA*;Eznf5O)=WJ&PEBpv?2=y#XP8-O(hZ64&ZrnDKyU#gKBt>e5J&tMX>u+-P(%%U8)ZsxcN|ve18eIpdIz zpz|pB^5N0x5xzKvl|z`)_H5O-w_D5}Gf z`2kndzQMspbuIzs1FVZF3&o>mbVp5#jUH;>mFSI+yEU|Vv}i7kE+?*~^Age$4bz5H z7)GeGVJ4`kaL|;HtvUF}RnoYE#*g->9aZC_+p88u8(Oq54$fV^cA^yxIZJer=sVl_o2e2`^g?Zb6mpoeI>7>A%({B9m^yadok;nT9 zx%$M~9|r!{u=zh2s7ndE??jpMlzMn9rCIWKFM^|e8kNJU0Mp-;ypA**=NcavUAiAm za?`h2L$YYFo|FkHJgKPnI?(E&AY3it8Xk_W>;1w3I4z#~cEt{_CpRhllMG83{gI&5 zh)~ArtVgHTZNwi~XQ0UZh;JbpI&Viq>or;VcpSVP=@J=p1GEk7zXwHO$bw1N<_W0h zEnv`!5d5R9Qsngv*n^()Pu<%;M$lJg*%QG)p7Wt zh2NILcJ%u_A$r*jKu0p91UI=Axl6Qgmu$ErW&6r|Ifhb5`)xUGdF|RYOyOlH zs^OWv4{GRbOIkvIX$!8ZDyPyz$|s2a8xG^6&ONv=J@B63IWbXqcEOBgW0M zUSK$F?e-UAL<6EVCaPBX&&vi6`)g@8E@ydrA^nr`4jxAXa?A2@6|3t?y|C*u;MFpa zBcy*ToqyT4s&%z?!crN9o6-#M(2RZS&DK^NGg(^Z3HiSHUXuH)S5Cbg<7?;I1>zYc*hrEy3YTf$cGLPS*3bi%6P~BReO@SqDS9QEV zrzR!E;WtwZLHL8nsn|U*&%JS@6|5!-zW(DRm4t^z72p%gSKejV?uad=S~YOdEI){HIM+ zG))rQthaLa7t!Rc_VKU*PDH9UPfsshXJ09CA2xl3&-9zuipy1iY5ptcqzdg>1cY+ObS#7|}$d`M7a>!BbR%XUuxb?n8{ zodY}X)!gpyBomO=l6P)JJP!ih{_j@rIY7Ye(Y1=Lgup-3--8pf|2?e!h6Nwi(St>%x@B z6SbBt^PuLrxx0*`aemaBn{23gC%C-5^Q?JQzXr@Lfa*VGE1=y?sHSPPOVYFaGI_bp zi+0*tfbyV43A|mjJmR?1#mTH)JJxu%3m~rCO0=z+S!-QWghp|Q;8!3 zBwq#I-y23DiXU!tL%M>HOc@>mC8HrR_llZpzm9v)PrLYH_P9JNU6Wgi#82tX4)?}# zi9;QgY)&)HeD-|`)Aw7U4XISO)S9gW{i#IB^`SfEp}KNs)@LGFoRd3cGu@yDt?#{G7>({lk+~uk53>voiT$#^=6_|@KnnZS zL5?AY2k#xmw|XqNnKw=vzR>lVzh$tXLoP9m`w`?;PtsamJ}?zq-_eg;+jp>~^!@CV zDCsF0kGRU+y+VqiY}0lrqj^AAdsgdKQ>TsUhXz~Nf6ert%dLt~@-erxiG{$1GdfxD zQ}81u_x>IlvWg4!b(`<^6D8&?fXdFF-@?l6Fzo_A(Yk8^_2+Lsj}nu~PSA7`3IozR zG|ST)q|{`5zia(Me;Itdek_~WRcdjs=X{K;1S1#2DedhZ1YM;eK~fu@Hhwr^qMZ#) zxC_2#ydFKsEYQ$mkc6}jb&D3v8@ot;iV8<`4;Cd46vL&^Ch%%wdYh=6`C443edko~ zf2{vMF05f-+nTzz<n+d zt7;N=aWpxz;@`F4oVQysA)~lK{+jFss5H0Wn;ljAp{4WZKbz4&eo$O@2Xe9XOGteNq#=!5yC^E!ceZhe$qb-`Z24Zx2$R$E!!r9 zJ}r`RDiWmESd6Zch#h>9q;q2;rLSK^z4Eb9$n((ZP% zy%Id)%p%izDIgxgHUQHs#w6b56r|l~HZu0qT2Aj$SM@QxgWS6{ zYO>%e=i)D}lza

mLlQr~+B#KR%|UAL%w{ILtdflG+qlUFme@3><;45zJFK3PSTA zZX7QdPIOr~{gt>${jw{4{L{f19R6Ig6}P6K@Ms^{kKz~wz48c^j;pk zZBceL{J)b?wk6-yK5RrK#lp=AN`W9lk9sRA-n9aioe_$7rj0x@kt)77W!P9N zeq!~?M$}|&+`FOo)|lCGs8C#(1g_Tak$Jvwd5N5>?aB9&WRW2T}%YQ)m!yr%6G zlxK?69JG%#18j8h=*s8|gjcfz{!=S^3scu09 zTji1yQbz*v!IA+*72#cv!QCdDs}PDU=c3q0qSq7wqT=}-=Kx?12Jey9){m3fOh;Iv5hV@FG}0($dn~ z>@^9>uUs8{z92^_ao8cqf+|V#|1ady7h0f-)d)pF*Ax}GWN%alsH&j4)F2h+z({kp zPY{a{g{?t*zh3Pqba=4DHT}fi5N@jZ`|tuH@nGx!_KZAXm}Q#sS2|=sLr$&M_eFgE z#QvX8{>p|bHPj>LA*KKj;BM*jo2_fW3dVUuS=BLD5#)vb@il*ZjD61`*-^0?F&6Wur`!sOB~?g}iN!5eB>dO`axABN~YHLCJ-o5s;-);2UW=akD-+40w&~_tyt$Js6G+OCvT9BXik{& z#bkAd9SZNf53IQNj{^W@ATD~hvXz}M$>j3u<$v^yy_AU+j&CX z8v2UGpc3BRHlJyyYYD0;ln|cby^;I_J!Qxwt*VJJM#F8=+RlK^SYyK-)GypXQMs>g z5$pJ-E@Uxl;>JVtHmCS4#4wgD0y*tLBPRLxQ5@Z2ZBj(3I1zp1sC5eqd*9ncteUA~9uojOoqignHbn@|IFi@`P|HH*}FCFbw*F z6IL>7jC1e4t}?pqXTl!qHS_VKz68yXy6_;#GmnI5RfOVN3whKfU>JZ0Y(eLX+7UW z(v>d_czHPv&y`%hwAh*JcWeSRV>^gSx&vE^RtTSMEq#^H{v1N-R|Y$c<%OF&B4Fy)2ncNd(Oq1dZXuVy~FW>^@%Al38X#G-It8r!GHSRApDA_kJVmdAWvPtwHW6-}Kn5&h$%8 zM1Djk40MGJXfwplch~)cK`V!_&a+#sKCKfkAqV31;)&J&l~)gj#jfTOS*O)_Vj;Rm z`v4tx%VMi1_O3gK4{uHu?qAD~X#^{TdY*AL)vI1k##DSBj<=RP z@Kf_rtuAgf9LIuABm+K%C0E#iK5|(G&2YU61V`QKr3u+T$td^6GU})zl`r;-7Qs|n zC~T@Cp3fT{f+G%0pR4b8*`36OQ+z z2d+9+=H$QosEiQvHhVb)x#?}l>z=W6m&HHspP+UQNOJZqp^*hVu2ZYAu=3v{Y1dEzvM2@^tl+imq=5!1wsRbw^P5>S2P zgGQpqo^2X=)9^`O5?(?ds&?)9opipIm}!>A8Bmmq-N#CxX_GL<_&_86&PuW_WL%pg zx*fZs0i^eBNov9gp5V-%6ce@A>e1AN7`ERtWPSunaq5V!#XmNR)y^(7Mkn0(Ex}Cy zuw$^BYk{sGN%`ffC}!CAS{?EOWmcNi=G%wv>=yMS z!XArhqS^%4t0GO==m|ZhkTcp?8Z#A()&o0Nu9hFMMYYL!J|-IbM|?al$>EHSs*_zg zrv;vHw>5?6}GPR=Jf_<)R8x+fno*S}iVr;{^GmHvbIDKzSJ7zjobI?aSCPK++X!ywW zKAA%t`>-_S`=^CV+FP+@aD5sRkr@|Gpbt;O$ATOCa;a=1H64{{g4nNLy9ZV)g%upb zoU-x&CAdYwAu_*vwCIFY93!Bf>p|_ijp7V z9y#h7a~(@loWWHsE}1_GiL7_aflEi2KW7iK$x3x~Ics;OUWNisV0OK6=(8Hn3t2YQ zqzMRHJ~+qh1~QpJGr`;fZC~w4>={NqFdnq@_}QUTkMqV4iALP^Khwr9W-dmVFQ&Lg zm3x&LauevCrK92|petvqH29}cd7^T?D!|(0~g8Y&hw$ey1*2j z;Y|Vw${aFQk_-ADKT((axR~5xf2$sj(#s2kAqcs6+I+Z03guEzdc2pLY2rEYR4M?rqG@#U+v#w3)rys<0_rsi$h3h3|JSA+C3&yH)hTyx*w<`dTIeol0K<9+C%@z)3&f2oGV%7M zmCnM2YQ1(g$l?KhN%klWhbC#+D;fM76Ykm_q}MLX>n_8ONyI@qw~#?B zr6-*{!q{>Yn-{NhWiADrI(---Wtz$LG&s}uo;82k7?$w3b0lCf4Y=QFS8x;H9@FSn-=UEJ?msOJV?A0*RIlqU_e zE@F3W?ALr5kXp@@bV)*z=(<|5o%HR8lvMdLYn$_>fuR9=4vm@;8=ya7IaVgWN|4g| z(=baLVhFufH+a^fqbw=N8Ra>}%_Od+3$t{w5Hl3Y)S|ueOe=>zz|Fk|C#HX$#3dGY zoYQ}WcNB+&s0;ymT@bWt$gGjm7tr1YjYJ)6ygX;{1Q8ZqH2+X?NE*yH{5 zr_N@i(*E9Ksolht&6(Kt+&oR?4%?upDDbPzBLZ_#yYZC_ zeMu>~9&l2N`FE^e`}@caZ^^MePntWbEV5w`Y?@uwJflrSmGO@tq$D6}_zbvJ(z5bX zwUs1`!Aj4$dY^?hlDD!<8+L?UBHf-_f5H-q>7Kf&JU<&Uq%DevG+F3k` zYx7MD^cn5Ak_IZ>7o7fEzo9PlbB9xld}gnEukyU$c*wHeu>c{g)1{)IwZQDgG}mG0 zV+|MeYMc5khIr`a)x(+CMR7$lSgrcch2t)v?#4dLs`#=^@Cq%Xdlkd;)=$2-M^q_p z8E3YumEkFwMz*$`nI@$hmM7E+D(WyqiLS&N-8#I3+?}h4aI@lOuSo11mnN~RP9Z{$ zMQsi+hD+UpgL)!+7ZQ%?&C}4Pn+jP)pn17Ma`Zoh^h$;}KJzN+lauE)aZDE1jFW(o zl1t5T$h47o+BXwl3mfCVL<}KPGfbgwyjnWzd$nA@_asBnLyB#L}i#b z_02eXV8PGgWqRjS={uAic)C5#6FIZX_;`tTDKqSQ~m_ zwFfDGvuyp9fzSY8VCJ5wJUW1%7pY!>%qgZz7}a~9MShg~Q8zkA69jV&b+n$4c6QmO z`n+#S^t(f^Rk%}9CJ0YyVZ`_CCCY8od;zay zLn5qTB~Q30LYG|t;j2T;ic@aNtu>^ZTgqU+iMJreIk+=aJh5ar@1kqb$nP56}jA40Q__n>#wYq9ku?#A6XR^OB=2{!S z+YqlJs_fIpWJ)X4*l%r=cat>~>4`zYC#i+7wVzkw!MLYvcx(pauXVP9P(n1=f4n@A zf{k5r*7@GJj-YGHyID7{n>ujuB@@~PaAW={5>=)zDq+j0$0oMX|80J>GlVL_EHPpf z{l%5Zx&5)>bGOvRnPncg)lpssCRd=;;vWm1rKGEB#wE_J!dofXpo*g|x6 z)+ft?iZtR~d=8gMX?tBHGPQKgFk>8VR#7?R*NW-4dOnTHE1D4+3qpy1h3r4H7R31@ z^kefaDJzR27QLV06IBIwa}3@wG8~5(xd>U|I}N^AK%Wio1m0p`idvTxxu?oAi_K!a zi3^QS&BoBnDhj_+HtkwGY;3Z$6|=vIRLFMu2B6T-PvNU@uK6ItRnwA!%VG%yv zztuP9S##Agr8iZ?yknSdAVfpM>nu^Tvq9l!Z6*)8v0dX~XP2+!feVGu&*O0dp4c?Q z7i6ioy}tj%(MnIhTcN)Tt`#tcdf02t3rS@B3@}zyw}SLhOeJ{e3rzNEVFt%=IY*v) z+`qZdzZP#BTG(%4?StoPS^oO@RCKsgyaoi-bC?dIVYS9su5Rq+lEw~ne}Yud46qUN zCE98RsP}yvOh;V36KL-Xq2v{ynDJiwS zoQHK$7T7#mZ#`BT*9m^=RhgM4)dDcRq@xl$oyMZKRQ=pe=auCkYfTF8NAzJ?S*3uq zM+j(9Yx^hnB8ylevQ^KdstxoiTTncrhl127u zX=xU+7`a)W<$i>&zu28pZznm#^4JgVz1IMbjPXuNTk1j6kDtf64AXiG$Ej-xJ@vY@pS7hm) z{DMB#0i#&5{x1O8tptd*_Bi{GKjcWx>ItVMg=f>VX!+znS=`OWE;lt6kv}Cy_a3bw z1$130N-sLNK01)^iC^pdX=Tw{7~xyF55^4`OS~EZkP$wKSxVNG-txIM$i+4d0J=!l zc{bYJo02BP>(xS9-?p21i=G@^pO3_BU>*2;In|r>pb^B}X2@Wd0FmSKAZ#GWY^mgK zsFFlaB<44I_EHeK*Nm4JRwb2>P;*e)FMxiqbDzh-dF;`7$u_G$S{rF+F5Z$|E(7M- zW}HHR@a=IUk1?ib`-k@cw~SZxE6LCVG?jmD zV)bX6iCzw6bDyCE#qBgXY4UjV$jj37l+>v>)|C%{ZayMU1Kqw)F2ZBdEx$PBX*NC@ z9wm*vHpn~(t|u@EU>DM*1L2C=WhUg`swTg!af+zqZ=o>hD2>jaIeC{bx@iryp`!`< z^HI$&Wf6Nb@;P>_bSYm1)kFO$QWZhnMmNVC@D4UO67)@)W_PuGi2zF81H3@UKRtU{ z(0>@m!^y$++w9!`r658ctFp2TcwnevEvtWCw(;H4$q&(b;aT6nP5aDdl2un#Z$%@_ zd-EC6!OZ|lX@}>&77PnxeDe>_9qf@h!n#(UmHs~`!VO!*aCGC6Xlux2TjJj4W1N*F zX##s5@mv6e6vL)kGG7|j#%5^Ys(KB3Dw`py)hk2!WpAvMME?9Rlv$AFkNc`$yU;Wv zV8wd@zU4^&7H?sS-zz~wBZ9Q~)d`YqZ9VQbZ<51r8Gj7Ergvk$Cd#wUf=(#?)a71Q zr-ig-)81XRtcA?UN*a=hxjB=9MLo<8%M0?x8l}y9Ji^lq$Fe&8j1vZT=-l7(U`N<0 z+Z&@gP;%%`X36xB=SL^wJ71%!46fbSkpjqNF!AiSp%TFHsMWWe+}an~1%63+4HMr7tr_HrUTJ%arz8QXn5=F0H#T zO;T8V+A=dZen^==H~t=>`u+C3tYi$8%&1+-1Y%!7Kf6(J!+=fA<6;XLhf2WQ zbh^xgMqQMi|4vUpbD}(a*3!Cfft*U8%2I22dX)C}1q(Omzq+a&NPQf#b89A_&XXD1 zbKQ5OEnCfB5HeKwN{$1F7Cey!AnH&s0v9O6v)2!Qw#a}hizX-$r8;ysSPxF87%~af zrB-F&`Sn`J?ITNR9~OuPXJ}3leVBb^s5zM+Hv0~BI!t8ifx2XhplHaMgN=BGLeM~; zP46zQT60{VIiiD|>xDj7@BHRdY08y(H#u|MGz<{!tu8?e#O^&#@#68yl2h29n*CUr zkJ6yj&(0+r?xt+R{8zo;r^qL_qclzk7iC*H<4{4Uvm7&^smYPDT_pNCs+MygucBBa zQFhPEjv|dkW4HdQ;AO$r=6g{Vs^M20c9&0zJ(hW8&dXFbQgx9M9lPYUM$&Sh(=~=C zneV&?+Qofv3m%8#;nBFy*1n7i%X|UMHdNSN_V{S^E5)drYL*NpWi%v9sckw36e396 zf^$eM3Zrtn!ql`l{6hh$`F+PQS2E)G_X|lOY_S{x!&vYJI3FR399Ez3Zb$1?RyBEk z=DZO*u%8$OR~9s1ZqWoXk-0yH;4Da(G^sMR-f!MUUM7&f}u2w z2bq~l3+92nT|H#|&9t=+M~_@X;IS%e$_P5fUr^jiighZ!swn$ehpNy9Er^$ew53Zf zIyEXF4o1YeRwVVumOp8!w|bdKMH+tqb#zUy;#Sc-6Ba)oQDRMr`Ezgq=2(Ba=8cE$ zrl~qz%O;Ho)OJU0GisNs7YPk(9OlZn{NZgCNngJcd*DD)~~ks{NEv=ALuojNMBll$mM^pn>BiU4w7K zjN#7V0y3C&&C{qnP`mO>fmn7GpwrrK#GY{Uj{igjhIHZYx=Tcs>3xgv7*&=B?69;( z8`)rF4-?xL=Q*z*t`s}A_Xk_0c~VDf)Wo$AL73h)`IsSjQ43N`RV8H$*QjAfO+P*@ zut{w%bkcuJip#~}juJ``U6c}Oc1tzDaT@37=(zF^=zC_$A3(CDP&sM3M4c8wV~t4r zXa~O(y?lo&4HcBZ%_FL)ep0fGL&?B0=&6HAy1Su-6KZGaaDIzcu8I7a!_J7^J}b zClO`JOv=g&r^OGSNeYPFX z_eMv2XeMIWYEJ;*hg=^ki;L>EL%&soeFHF<9MbWzh?)@OD=xQ6lN5Y~oU1KDB!Ci-N&%`Q!2SKO;Sj|-=arXD&o zjNhZ{uw=Dm{&p5ALh3H6D#M#*6&#zJrVt5ArPkNjh5>{-f{=zZ!-USD9TKbT&v~dX z7fywJYy-2Zo{31lR-uTu@_a+S^y-?#2Kn9s0Xr>^YF%LxWMcPK zOZDwz!<*h_)HRYwBOWm?eohw!PNx~Wbnb^FGK@Bl&ktF%J6GF~Zu;EtsWh!#1*#3M z96T0rJiND`KmON@_<#GOW3gkNS(%u=n32S;Xl$4HbrSs=%f|hep9R_F&$?Ga$?CMT zXdIF)PT7w_IK0B!nh9jZ)pry!@|k6wmo3@8a;oU|3xB4|D;}$8?nON(BGiXY!D^en zW>PB`yKu|uB)3G@Pf0e42#XHYOJ_B2`?~5@*7e$X=?fQ=S3--ux8Z-+RZ@S7=2+(S z?2w^FaOrNih`GktP{8fIz7wauC~KG>{C>#@Ppk2?477nbWtT2qGOx8E+piEiZo2NH z7&f#sxvf>8D9QKPsH+j7J#C%{*PU( z&R~3`m@)d_e1bhQv45$e1+11mHgr$AMsy5AzGV@-ejs z(1~G*dEXi1(dghT`#_~eXHkWz@AFt3c(aQh7|2s+r&p&2L(``Q7n{HWQZUm?86tC{4m> zK1gY~5y9;EtSan}Fvl}xsv%t;8+y4WZdN01>dc(n>4aBP{59Ed^FmlA<>&+4re*R% zx6rw3+ypUTG~~4V#D7qdx|DV2D4cz6D6%LRo*#t$Qn*5k?)~{A1;KmAUpbVJm`{D{ zK&NZ3npu}7n}#wlGZ7)$1k!sf3pm+4Aadg2xPSWuF5_y%o1ot6*``vRLClj>of~)x z+`enDI<2wAoA-@7dSI%62m7URryfyg+g%4V5_+kteU2j4qrtbN*Db=vc5TW!M@(jS z^W!g=?&G@e?~SJ5d{ zknj(IuGN*$mghH@S#CO_zui!j*@kZHv+%F?P4%#U2v<$Eg?tmC*R}QG(Hmh$YvjV+ z3TtO;0tCW!>7)Sp;9hPgMWPUcoFYg|Yh)YG^aJmr4y3r`Pg?G=H#z>_I)^WfM2-8N zOl@hWpjJgS>l0^@ps&6S27qosjbEeJ8Xd~F*9;N%OE1Y-MO|Bfq9LO6NkNTQoQWQY zuj%Vb@SoPRt&^+78sODaG;NWK$3FzDC%;D6&5dDQD_PPn?BLdB+EBTjbgdt*?)X>2 zlv&=5Havd>i8%4EY;eMrHAX+s2e!d(C%kL+C&0J+jW1bO2A>(#;5cbr=$W=(8xh7N zSOoZuhAUYml88qT;;<<`FN?Tw{69Z{XdaD!Xzydjl*-D@Zi{Sqn>U~ZI@J$Vj8U0R z7?V=mq=egy+GH$v5nOIzr9c4dB!1V%dPOo?c`-cN#)0%nrfX&6!Gj`WZ|bBT|4=a++8SY&vn&WTpJ)!Wq2DZY|B*IxPUA9r+@ zV&;i)jggRKMNIh+n*J_x7(=aL5-61S%Lu6m0XQ5!U*yb-)y`!q-~Rj=8M`Gok(o-n z_S)T$kd2^s)c##~$~rJ3%EjK9oM|fugU~%{2DPul@{A+KLoHIDd5pAPV|A)HEV4-P z+mR-F3#sM>{f@g^2csem|0w+!89*7&%HqE*8U$d`hm4MiF=-`7i@hSu{q5!r+Sl1KYH;)G&Zmc*mYQ(oE>WiqG!V=RTS$T zv(P$zLD=6SSFcvtw>xx$rov|>=_!i!MR0QOk?$Fi$c2yzn$(iI83`bYD|QgZ{T3X! zT@NCxo>P`PWZ2?Ox&3u@ZfG3{{xaNv*i^hWbwTCT*_h@}Q=MK3g77laiAo3JbRN$dyrdSnv&2d)E=yfJJKkionDkrdioX za=0SSuXiI7?5)1HbvDGm-Ki)x;a=($Y16Bj#WkeRk;(t!5U{8FuCR~wQLv^<_lHbB zOda$iX7k-e)3Dd?-9cvqHzeRB!j!H!rpXHL9qzP65Z6Dg8{( z9Qepl*nC{ynC(tCQZUl`>1-NkZANV!@u!BV17)o!?cIL81yT0WRr02-{DfTq=6%T4 zkCr%~pXavA01ERlihiA?TmWVD+7)%oY1W1^oDx{nNy5_D<>#H{77C^hdM@coY^408 zOd2$Ey<5NIkGyW&ujpSj>AqJ*VlIzzb`G)8EiY9Z52Fe_y%o^X%=?F6)fS6)v~c>o zK7-Mr0hbHg6pxJY+(-Xf5zY#KXwJ})kfZo3c~B@M^`!U@LErPs(y5wTQBS@b1sjFi zTI4;##tAb3UMA7@WM7UFj!IDJGsJxznSev0_Sf(-hc5Vh_VlDxF)MxiXyp~kG19wx zN7o@m1x}Y&4w11Xq8I0+tm@I&|A+ANY1%u&wCY}==J@39r)2UE-RC?-dqr6N1Lv#K zSV2#8hwIN#Exz_7XI~pd=tTK)|MYcPL9zecI<0Ty7wTHlFLYX9^|xIYJ|Kv2c%^Oo zF0&U3S%{}}_qOMmxNegWRg(AI65npv1;p3!MpYpp^=f8!VRuBoxjX%C_0~^2nu0TL z2=0WZ@lbrch*N1`&NAC$#0(9&z{T^kXPB!#T{~MRpJ-N+Y&jkoZ<{%MDl4}o@2sWi zJAx%tJ_W)bPz`Ph=z!@>U%uZ%i%d}rp5iA808@jooUx?OFkjj$XA}_QhGLGQbXOS}9xWL*w<@i23wx}& zmF&{UR4DteZBnI#j^PBWe*1Z$3O4Z3**(D z2d;xHSgOCYA2Z^bS5;*l2vMr9*wsUnZ1OE;q-!vIUtHsD8_;gb_Y_h(2L~0EK*9tS zqrPW7Z*CnKppBM}3}vBeAC`TzT7}_p_bTwkO!9l_Z5iy5qg4PFQIF+a3=jxNg5#^M z>WQtIVTz9L;QXe)-qY~G6v}c$YN_G>s5TQzhz36hG^53c|@3-oULEqgk(kXl$&; z+vw7kIvbn)CVxqVjczz-`GPA`uz1JWd~ytLu*U833)h8b8Q5SPsnfyVPQQCiT2)PD zwbMC{A^K-KnvrkVwMAgy{BCx>sVxv=Vvs>+)E>92DXH}9%yC_<5PCf>9+A9F*5>@% z|M)$#`v~jFCydoKmh08i2u+AI&5Fao{3 zPyBy@L59FOV6Sy0RjHSbkA@rkIe0^=ZhoHSM_zlSJ-T6(^!;2&O+JDIDkY|*# z^s>H_wBU-GEN~(jd+HztgL#wREVI;DMc@E@#U? zeFI8uO+TKJ#LdToWv?c>|8AN`*wPT|!_Tb%{_PT2@Ic&Ay5?`K)~qxEWx*eckY5DA zX_k>gAQ7DHY~=qxM=!ZqM~1_ftaJ5ZYJ=Gop%91mRnaz^Y8tcchi{m{9p<-T1_?w*nh=`KCfLl4f z)mO09c#EZal!1!#Y9iV8pZnvjU%I1=s;J(;>7=JB3Ll~sev1eNHNJ$Y8~SL{qsGFA zphJWA|6r_qA$Ikj{m#5(QCm^-&=>99)1b&m0A%=|i3H;+@%!M}BJAEF(70eYZ@-_o z1oBu&fkI)|6?(dz<^IP@7CYq&#ufuBzV20C+lW)I?HHwf%b1maowLuU=|z7uY)_> znTSk^@Mx7qMQkHNbgVc+v$<*mL?QF|bYH&R%iUnQRf|oO zU~{pZt6&!R{T4dCALRJE;>JFr(v98C%>DiC^Q62tsC8g;U`XLYyph{Vh*9ij4!dYv zXV`xYBfU3fOtRE8b@kExATqJT$Wv0oBxwL-w*0=w-a`b~@EI(+Bl!3etVr5>d9lXD z@O##Rx*kuG2RH!tpA;6PyoeRjzAWL2u@A3NR9#?{c!2#U09n)Xp*SMFhRw*-YVKdo z#CTe#F&MP-IQkzx50Y+vAhoSh15lSi{vjw+hA4QZfP@&RVnIBOl#1v4G3}OYHNCsB zhIueuBt>U--u9fsCYw`{$Q%pJ#$J8+Heq9^+bvXN(^Y#$*_izZSx_|88LLy=IrMa& zmZ^e66v?C<4uwny7EB5KaJZbN4LcUPRAvBJpz3tNH_j|?HRyo)>UxJdF1#SbWwnNB z@Q&7DL0NR6`7vTl>v>01m&I&vHk>pT$+kMT zz?tnhxF)#!oamBrOUuLTYP%y;?q)=eWSu=Xpe*3I>m5(z;GCJVgnO7Cdb)-fR8+Qs z>tWB~{h=Yl201q)0x0Zp{;8W4BzU0S&yEkC21Z?MYaH#3DU2-+Pb~>5MqsxT+3aFr z6%~e=-^i}&k^XQ}BlMDvl)JMib@7?*qSF6N$4 zX0{Bkcapk$lkRcwUNYiV+7O8~%wUIb+ye24Zm#OhO7RPk|GVRwFV3IBU@I~8Z?&nT zzpd?r?K!)4L{tYFr_+Z&0$>35av+H+n!~Bo`p}_9qw0tHj9p;pdnKu|2@*(5cr=#) zDw$@d%CeGqcq5KrQoJvQ>EE|HbchmP-`LrG-ze=t&ra&7C8b-IgAUIJYzG~F!2LiR zUz&FNZC}k8YG;f|8iEn|baA28@d=hCw>rQsq8T(S)#}Rj>*`X~Z2?@kG%Az+w%a%# zniWAL@rTK?Rb#J8mf4G}F;Oc?_{M-13+R$)G=)!~E&pI>NFY-;SR9@%tGDe8Q<6^2 z*V*A9Xc27h(}qpXh;xFp98<*T(*a)*T2^hnD%KS*cY@E(`6aFruarlb2cyP5J|&~d zbiY&<*+_J_P25eouZYxv>N}LSbH9UKu7`&V0WK(;P}FK>FZBkkaS9ktc9)Zi5v+2p z9ul6+-qO0747wB)B8pp*2IacYn>=7^mIyp<9L6SN z7U~D@;gg#uahtLfA$LD`CA~CuJJNIcnFdpRq2V(}CD!gJJ`QX&-ZkpJs;KPEiOB8X zPLj;bqxU2pvjfhUHyjX@_nG7m{328*Q!T&G4{sJ#2%JaXF?P_tB1`XcAU!HMI%Xhi z3tkm*tuSU@&END@$j_;Y&~rSEfM$etSZMpB+;9ug(N%jhHwh=1PxniQy+3?P)9qR6 z67A=@BgtLXNnt(ckrhSizpSNM>VAaUdL^!GB|hRHouZBv&dRH zmeWL%RD$8)oG3?~0B6Oh{XztG47kk3;W0w;bnZh)OAFb}p8+(-&&wEbg+-gy1J$dd zmd}|jWq2Xn+XS_+T1o9jq=1Una9!_0TQV_3Oj{<$e$zsGtCw6J_h59OU1`yf9sQTg z`q1$Y)B9XqdHfg1L}fqpVUQ>Utea;&RB-e+Y4Gv+lpp2&jh(Bn7JN#4Zq*jmk>pr zmBY7wh?Xu*BH&98ZHz3J*pn4nXl^ySo?{!R{W*MM)Xv3{FFS2{g2y3voS8{585^}S zQmx;-)<@Lerwf6+Oq{=NaN3v@u#Ym_w~cme<|^>B<*SRsY0w`L?5teLHyxOmKyob> zA@uR2s@6GEen+;L>A0=M;%4GvKaprle1uCkei-R738n2B2B)_4zBmhp) zqhljWPwr7vwzVzSU|sk4Juq8y#q?&T5Q8g-P5Mol$tYAck~a_1^4wmEtS*@@@X#u% zPixs4-Y7a`X9t33oY!;t^1PV^pLL6R0!2x{5N9+m`--L| zNbe~l&fm?)%k;(ZqV5sLQ0j2xa=!|B}dEHeYIIt$$ zGX7l&Vm;MI{`zd@&ORhe-U?4RtMKyuG+n39g$Lv(Y2>P|+T6ONuoax!zX}H}#xIN= z0kyicG~Bf_XWLcqm5mq>x>du^_`(}jX~F*wH}iFZcjDEd0|ubACo>U zH)C@%0KqObEdbO(P9w+88Y77%s_a;h9xSMeZS;#6BN19d_5NIvu*!0V$UYa^@)~=zWLkxqfomdhrm2{IGPFJ9*D%WD?BA^lS)YP<8@+)A}7l1^t7gG%Z&1noZc;B*YAB|lmX}?>OX`C1xmQN z#EcdmjOhMaeh3mpu*28 zjufvp^uqV?oqvn||A&5D!}XPd7m=lV{T|Bm;UmB&mh&Bmjpet(0z1R9W(?$ zLe0-nH_U8R`W5SDM5JjfzFuxi&8U{P)QstBuAEyA0Pinirm7l*A$p^*m7z>Pv{c@t zKhbb#4^rSafE&U*cg&k}Rhk~_8j{aYl;L|a@VO0EWb?I_Y@Jqt+Ig%v;DdC7MS#&( zpQ-2Zk+p%fXb#brGreb*&9;(~wCo^)7$#EFj-ZZ_f>7hq=eb<#Qa8R^>SRZ(JLHad z(<3_Rq&hqb7wg4yMaUGklDZJ9qGu8A(lm^u$o9I6_Hs||nOk3I=gT5R)ZPIsV`wu~u#`%xF1{pRp(b$4e8PR*Wg=*CE zP*X!)zaI}+25gZ-v$Rt7o0hkg0Asp<__(9c<_UEAer8)PqSu(R!tW zNmhM~)5A1Tl4n1!KwjkU#N4%c%a*`X3zU4z@#BbGFY#L9j+ZYPD~lypY1~EtSyfR< zCE(&OW3#ur3?-G97?C;$z`lI^En7_v=(}y3 z*c^1A6ITSocc=1z{Hj>P8GK72TT7uhOyyk}%i*@k^ksZrFRlV|P27VmNud?wd`9hX zW}^*j%82DMS0ln(Vm1Nzjfg8!o0)$UrOI*(bj6NM`UOa&K_k7sjYz8HmBfJH)O<5T zu$2`(<{)Yfj1|a}($oBQ9Dne+x{ZutwDGr-(r&3q5g4~Y^gDvjUyNat z;pHIJccTNAPnQd&BlxVL0?HYUJG2J7GICm}1zuLG0LIL23%5w`cS=$5&2?xa7?HJc z380n5vd!@!UBF6ee$dO7V%3}Kh7W7*@Accr8=c-I0A!AJ{PIrdSF}U%-Nh+0-F~(8 zc){*1J=g3@yXghS!HO6z@zZ#>zHR2uKtA_MH)4y<46ajqVh4s3qz#M-dB&v=?qxYI z>ra0~LVzTaa*yptwrVY6Qa7mFzLDy@uHcXl>kHL)=0+~u_V7iU2YK;&IkgrH%@%r_mA zwSbd(^vguJh;|*U!!=@B zxeXZYc6E2YaQzOZz9GHi-25ziI)MGr!TvY$68IcZ6Rn~=K@_u@8_eQnt0+WUvuh8xDd#{*J^6`hLjBl;@Dg&W0 zh=lCuX=6LmEW=Boc#~Kf%)Xb_o`m322Hu-{e> z1NH4b>Kaf$cfIElo4oW@mlANw-#jDmk^5-00Z3yo`{?&_i@ity?YK-p-pISi80()b z`S6lAf5TwgcCHYK&Njj;bvoB_k_H~(8@{V*vlxAr`ZGIp)|8dVsTOE%V1wM~&qsM| zVxb+!b3ItLYOD8tdrhXOc3~zucE>Lp8{-tU9(g3>G~Nm7uCaYkBehAmZ4x68Pkn2y zdd|qn^3iG=oD-UE=W1U&a^NgwnB+k->1Yy?bfMy~gv`ILv$jXD!!#M{t-H?ilkBHf zJq@EK=9KcBvLIw#W`4H$QVT~tAIf7hNf4;)|p}Hj>6%6v9!u zYZ8$Dh$py;!BYY9=wlYc?sk-DQZ?r<4S7^*d6wJ~=$_l3W<0ZpIT;lQxc@5Z*$)?q zs)zlO*hZ1I`mzXpX_9_|Z?PjsrPxYkeIQA4lRD|)kchUrcwpuUA%B!f7GX@e=Cl&3 z-WAy=makTuZuC9iee(BWat#}sbRGP~}zvB`wwK-p(ry?`k(Y#ChHrndg(P|NBd@AlMnX1f@Z#~w3rQB`Q z0I!b~f!(1Vl#oQg*Bz>#h7L2|1r#}N(lAjmvZL6O0yF*n@{pe9phe4qc|8q3GGxQ$ z5oJt`R)yF5WnK9*I*K@o%Q-8C%^tneatZMJo8Syu0N8d{)5 zJ2JHc4Wp}3QV_V7Tu_SQGeO6jlaJ-SZk{y_n15M2C7K(l3#9Uwq;u9f@ycw0-_4!% zHaWZ!91wL0Qbi@6M_ULSI0#}jW%@EW+v{vB zoxAr*a?-~y2~BKG%EN0>L6zm))i!7N zeJEYL{jZLS+C?E3rO8a?ILGHi82qIDKT;RBP2RwlikWYB?-2V$voukNhh_I;7vI`{ zC%Tmzd@W4uZ4AIBCZ}w-;KYwQV~Gl3HRM+aS^M@$TScF$tFf8LdINY7a4kd}OUjNN z>_te~v%v;sC;j38Z#>%h9QGmQ;T_e!Yg(XV5_(vz%oOJKn9a)XmFS3;7N}JI7X&_T zYv7th8AIyl_aKzECYyoOIR!#L`tqB{upE)cJ4Igh zY*Lf3An|tF7Vmiz8ww>vHJ;v!eeTLW@_j8OdYH3^jiF|lAK62(t`za*Q)cLa2A3d} zf{G&WG`<4&_hXFkadpQiY0DBlv_ltaHpYgWDSuJjD-k|kc&^^)@15iJY~7aU?nnWS zrdE0f%191h83Zaoka0OZhh}pPMnkscLgOz-5hAjEpD80Z6L7w>{yd4eWF4!dpff<# zki&UE?i_O;GjnZq)>``W@4PRicoX4o@-h(9i9L@mF71j*w5ggB>N!Qm6Yr?EdiOgc zus(!PD@QFzD~Amp6~#V!vqyi$M!?QUO72NM-ra+aF|wkyV&txd&yHyk@cAl z%Xh7{e7$`fHR`Y@AkBW&;`Wp_;R)!yyRWL8`|jeA9Dhm#lxR<2{W^Ik5P4+c(4l^S zWH?H>TAu;1o5|d6eW^*j>5y}vD<$=^dn*yNs?aejQ)feWsi7iNhBHuNy2i>*YPsla z1`H5RYhEg4`W9;3k<@KDX27W=OUdhOm+s5=$B=G#zis5x==|}_;Ekc&l1^!c*Y^{Q$veTsBdS(&YD`65 zkO`Y9hVGOH-^bQfQwXnqT6ILw?vrXr4}odZ1n(JnV4ux3*M5`@UWl|vRxaF9f5rFb zHaobExp>8=Ea<2?lb^(CCxPqKykXLp+9<~JYj5?lj@0FnNklZNU2dVN?N9;$aO9)r zlVYneWD8;P=rvEn2LxSY5AS^UMdz9XlXD9R@*i6D$pf#62b2ad1M)Wv;e`vE8Xz~( ziXKWMI%JB(V1g%#n^7!zHrRF#!}e`&3=Op&rx#XWPi;)A)^CPX#Ot<> z<&EY1zTkyPFp9vaM4qR{{htpbBT;Uaq_39A90B~oP_TEjE4;*07L*e*18-4z>@0mG z8W)cr5(2So{YYPp7EHR-18^%#t(`o)r1SiGA75gzFMK(k=TU9Uu0FG8}DunKr z6(!C{>HVov5V0$a5#S)sC`SN1DQR!Tjnf~ShH>V0L7aKt|0rE<_KzD2g9-uU!1 z^9EyNK?Ez0UgVo|>0?Tq$C|8xX?KGLoznG#p^WrU%t5XTtJLoI2d?mT#vOO(PS^ti z=}GY$afJBqENYDs%N{{yf{oVu(Sltl$6D#C>QG*ODl=lDYF)-`8|&JBZN}iBuY7(# z!j3Wq`MHez5O|RVt3&#SAH3K?wAVuV%!3~x{F53r!RK{qCF>-L0Ap@0X6Ke; z6^kCSZM^n&J+gtPxTpv%_WB7V{(}2#2moWv=uD6xqz!Es{DZBcF}#x?F0Q_{sla}l zRyF~QO)x&Jb2<*h5hE>W=O%|RXpnXR8moYiB#>Gyzn5D&%85O0yK`m-CPGMkWk<%T zk|d<>FW9*$IcAIq^bcX2@@4BGmxeSB5BL8z&-*{)y-N|)Of{ZW*KF8!ll5D35T?FC zk1*uwg9uja-!#opyaH%_IvYC7J8D$_E>;Kx8SWG$ z1nb);{Pw61KHlZ-lI;(uPsi#j`A@#=1=o%eZZX5=BnMo~(DJznUlLtUGT$8=FLj}5 z%@LTFZkR?t8CBw(or~>(1%K^^Yv9pyvfsyUt`Q&~sQ}ZA#6>IBIi}S;VbNee8J59{ zsJ8a=2zvN^)IoNX9>odKFXQOFv?Ckcop0c)16Hy;&3k?8W;!`(`EL2)+LhRu%WLF{C{%VJ33E zC!umzQQYmwtW^@85IvT`XN2@tCpe_)lD<;&g#mCMBwGbCe9 zU2M*keO7Jo$Re@t!V5A2ckePDd!Ys?gF_}>m(G|x3n^bK^4)^Z1t_*XT}W@#vSb<* zfX21nhNL6ph#`>O3%@){{|-fLi5M{V@mZ##I!Ie79nGmffEE|d6ljhZhCn7_d;o{QIe9pG^UC%g6ALK#5-93MD8bip=`=A#rD#wRe`A2&1N*75 zXo^-8c0M~~sitU8<>ahWZ zSq`U`8uHODv4x0p=U;^Xp1$bc>5KpO^n`1t{oWWe9$hLPb!qSV!ba?h@`^*-+kTh| zeybrfdl-T&Xj;P>HoP49zfFY~`!QOR7I!FO?G8zwUrFT=j2f(TKR7}<(?;InApDl0 z)_HG#*zmkovUs+2Ve{jzHoU*u#-ee#Z&OX88l8B(ghu3KB$KONH{;I@7M4yaJni?N zsFc7<{C=G+32M1rQf2X8|4pS2FFXpGzQkPI7$!`WKbD&DwOTV|f}ooEI&529o$H_F zwY-*LhD2VO3^JWC;;u7MBJG?+ZCcHlXX1JambPvu%DpeOiZ+u>DCqAjzR8r1h|>Fa z)PJmO5kOYDQ>H|6H7y_Y*@|wd3vZ0gdq%D_>LY855n-i>s78~2qzJ0P^lod_JeEnT zHla4vvR6ZV+raqd>xFvVQ^uOfOBDFeyZwj;7Q9p(p~%o7P^EhLw45}>GRPLJo9gar zb14Z*!R03&9G_l_rzXc^bF9ptsbK$?PW16(GF($pTE6#ZQclsh4T%=B68$aR*X;T( z*+%n;B*Ra{Y^n;|;u1fd;D2zG1<98ONF8Y0{_5up76%sbm;{$#Bg+U1N}MN+t3qHj zqALBb-aG$zTpbO(9KX`6N6xoe{CIM)+2M~Sxl!og=;eH0j;mV8 zu-65x(G44Sn*NL6b%!@yiI-fvAeAawdZowP zP-T(8mJjolC>Q9;m<#vTfb=dolyN7;wLE*TMasN>e9gW-hjdC%4^#%qElXB~bF1_7?CRKi8k2He;&YRx9c zZbh*wRoG!iR{B=h66x)L;|s^lz0j(*W#k+U_lV((@~um@T9cBIR)|&?x?{*97PVr; z0nTL88y7*T_QcL1oj-KKiY9}U2wUmWOl7u`b(|&b ze<*g$j63ny8@$1CUVFEXr!g_V;LW>oJ7Xe#pIj)xQ3>%CzcC{N1XK`kK)sWmWgT%| zlJa#)L^I zM*V~j^qT>+M@BxU9`Itzl%3Oo2a01^Efe3b;Xkxlb1UENAD!=Pq`sp#rexsHY%TlG zZlE_ZQ!maBQTb_0#6EW>ZMo08Z2zeAU9GM71|-jT*ceIyeVp4f{Yg8AbAPS@>q#;b zEa^cKi?GA9$dDp^W!lcy!6cTJQtLl*uS(%TSyXKo{H&Tq>Uh{TIyJh@RQ*6;eL#}R z^z}#kcvnRw0K>201+RRxhA@SD@)(Ef;Y$R&ipXUB?9=fRV~6K4P)_%8(^k0UDUG1S zQdi^G3LD#D&afC14%%Bjl}}N?ngF6HV#S)gGM5Skar@PbN$oSgH|Zy$d@s1CLUokvy|ieFxy4Lzk5oyfvcT?WL}IoTFHgG$PZq zoUaFwH(!5s-#tx@5jrBQ+F`9*{@T`v&9FEfRkM3cNsRL_7qP9xNV%kPvn{tSChsRa z&4>%*@!ZiV`sLDAvhmYX*H*#TW^-Zo!Ao1F?q^1p=wAd^jIz5yIMLf*+bclGEI_*! zMCW(O_UBo_y#8l~or_{?c>5L;yz~%hs;~?Z{Ct^M_p_kmt-RvyLjJd@+H8KQ#x#hX zCA`hegzR}f;4cE7N_mCmVUy?U@Tc|c@`qW6zX%UeMf!6EKk%KU?~2S4S{h-L6I2N( zZ}chK@{7Nv?UrW9=r5emSi}GIY4_nK39=LZp_&5ExCFn^Ox9(IR9o)i?p~`S_3Kl& z`<)5*ags-A>OHT7A;LD5ocjzuj7Goiw}F`y!wE)f0jkY5j1nh1PF=MBP3O(p2~2s!9d*(}i?x2pDi!% zwVa_5Fl@$Iz>Tap??OwmN?7{hdXN~H2Zd4!3XPI*tKi-^R-Y-nv{@gKFl#A!+IS}? z`|IaD-*{I}FLifwxCv5<^Pyk6>oUKiVEHf{bs+`L`BHOQh)~p2tbBko3;Ce8*Fq)! zIY?5P5$cQNJ6zb(`~=n~>%nE0`dNL#(0|ykeh!=)u5>+iQ{woC{l)@NU^CCk4^^U= zy709Djk?oen&jN!0OYAr{VPrXe@NSNOf0)FB1?a1Q(q8VQmZ~dHnypb&m399d6}?y zho+>Q57Vxj=ZwYZKk6oZg$frzu@pA>puJebr|_*OZE4^SHK6koiMF;QcQG89>}eu< z`EU|vdPH4YmP&^#3Kc0CFo)BeiY-O|r8)P@7|vx6$L?-;F`B;OHr^xFpOP60E7 z_O3*SD(A1}=G%dS?6PoNn6YvVHc7hAsx~YTlXiQfkr7+N1V?GP*$TdU-(xHw`F$EQ zQH9@)|4`y>=1QWv1~vHgD2MUCCKTF>D*T_Y`>&>obdMrf;BueUTLPhhNkJJnP)jZZ z=@7W@c%3gg&cQSAhlZ@&&VmX^%H-T&g)-?FShs_Plw!aav7jfX3CEDNT;hVNs1s#) zXfxoS3)U;#L-|i#;FTzCJ&X!n<5PadD22F+#KC;4JW^z<7dVSL3TIIT(Sz@w9nQC+ z<&MvYOXgZ=-lqHEdxFqW(e8dKxvHhByX!aO*0qppHTA8zX>w+g>(a!E*M8yuCUtdJ z&Ct}_Bp2nCAL4!$cW|b5tySr+etWIbLaxab4hK<-9#G!oE_OBl6XS0DdIk35`#v4O zn*R@UH6q9P$Du0x7iAszUzGLm|Ddey@DXzJ|L-2jkr>xi;=wfDi`P4E=_=`K0ACBH z3lj<|e#TAc%-Pwi!fY5!BK$G+{H6KK^wXxjNC_^XuoHle8R9yZVo}q#@cSaN>%QO+ z&%1UtKoOjVmT45k9(#x~lK+@KZ&w*Paxs74m9O!?7@_^t-;AM>2wX(_d|eNMDzhMx0TM zLtH3Z{3J($mKG-dR$#f)Go$TSo!RZ5=ogQp&sldFM2QV1_K6 zuHwhT)(F91cqX<4l3fN7Mas++$yP$~K(+N7pAQ-1IJk*vliJ}6{3wRJ5zyaC;`}Pj zokQCl#gDq~ZVgk+ z^WQlhtJYjv14UReFu zL#T7gYV7cUc73APJ4kdaVPD?WhlSuJr9)o1>5$n!;|&;~81DKUMlnRd1lWby5r+c4 z6*yS@s&_idji(W|Cv+O0mb3Fa@^yZ<I5Er>*8o?l)O&?lfq3L&1Rccp6LQER56fwmFD|z)Nj+zOXAJmbvK4T?8vH>adw?2f#$wKaT+VoD_}kr?Y*O{T z32)Xhsx=~~JsR1CCX0HciDO`g#xU#wp_j-Qrj9$Ou$Pe5xdREkB!O?gS+P!}1ns0K zlzLL^r3JL-_uTEE6Fm)~xep<8RT>HZmfh@+aXxNnc;5Ao5?7t(QQ>Dt{X` z2B_smD&t1}v$bAm?L*0(}Sks~9d9or!&n8H0Qk)2q(>Ox85MfxVlg zF7MC@mR_W}HT5qcRUbdkmh9QR>*N)B&IPj27v23<`VI(&2g0$7 zQPCLBbDqDFiqqOQi`SJ4`ppZ(pCQ;_Fcqreokr%pj_Ig&JYaBpcnI3qSD@of`FK_pU1 zgu$#FlU+jIX_(jWFr3`Rh&m#v=at|@m^UGc@I<6=>|Ihh4HU)6wq8+kS&rw(jKhxf zy^&Jj5_PNcO~NU2W_D)qVXct4<~t~}A>J@qT5eAJsHt_FR~Q@G;8xkParoH%p&kS~A{-d&Q>E&ii<{(N0I@x|`i1#cMxeYkxw0 zuD9)VY*(vDoivur-HmMC+Oq#T*ie@vYp*5fw(XYxCvrd(%>sYgo5*6>!+Trmam46d zc5Eg$*kKV`ZeH(xKPOrlYr96+KJZCOqMs;7S^8w52m`=5k*BlG?1g(u%ORO|G=q8c zyNM$`+BCE>iqhjf4Ku3f?heiD8ML2l>Tbk;?r1~*9fBS#Nf`isokyH zX@j8{MJ~O#vQn^*3C)t-4`D=!q*|L*$yBWzblyDjocBr$7qKM9D z0E|?h=R2FyROWcK2jQQ&FHsOO!VAAzRU& zaDGD{H^@euKh)J#IDuc;QIf`1jrXceRiv{UXxC+&dfSyMYO1L&@B^@^Nu(#Y_O+1z z$q}Zq4nJm)n-~hoQFOLD{WR2<5%U!E#f%^xVUBQ@zqIXsV5A*^+O6(AZz>g+gjgom zcC#4)yrJiuHfSkA2B*+AMiTOQ<~M$Z39S#|Uj%uQhU*h;t}vyiKG+}EjTfdMd)9Do zd6&+r_~;|Q*Hy`1?#Vpk2n1K25B*7Sle(nLnpm_jhH)XEC8(lxesIUWr6K3HFOofanXH%*z z)NR7`FMmjgM+?Y~rzT9^^BereQ*+_Vj-1O+Z18Ni|loEw9uu zHBD=bk;-wjbLiQ*qBE|Fk3x9ToW_x!>>8EZWGt+hVX-nb>p>^P5p(LTiC zh=Usv)qVZ7X`)p6N1SKl7~8c&hcS&zUZyB=lBv6Oy@hU-tis$JneS(xB@!6CDHGYn zmL<{>!qfcpPv;qDXy}~HbxBE9ihZFzx&zkm1#q~-#~U&X`wG9|kWsA_WjUg9bgU^svfl!iIJX7HoZat1(e6ZHc7jrXx|%k*<@(R)Sy4SLW(n%sQCh()P-pj+ ze06s`W;Y(RQP&(f6;2cjWR8xz#_iTR*4n=ICnWa#RAR4YclOAL_+?hz_+9&Z7Axx= zcX12Q+px7Ef$-f3(UM$OXX(87@7J*i&eZyDT3Pva8pSCvbmMxhJ+e1wzYt7`&<}vLa5OA^P32#@1$Ju*Fh$!6~T<8Yh zOZJSb$Jh%qq9|r+)OHs86Zq}ky6jB;MM&ay#e~LD7$}Qt6=HiQmoS|-!w&O*752?L z-?_mbcQLcrQ= z8D&}k8IN7wD)f+e8HyD-AI)+4fYEAO5}lz1V=eLIyd^g~)8&h`?~vFyzqI}hNS2Ng zM#eUB=Gx3z=R$}8p4KMM0w>IIqdjgL(|_(lVQ0v)BUg455ZA{Rjz(|c<>dW@G5a!2 zu&{JlfCjm9l3lh1vC#;LV_-`$qL=`k_(>8qPgy=a%HS`+JV>J@s9pFq%oAoZo4tOM zI@(XWmEUjeJkZlz3^i0nf_ysGt!7;JL!Z$NLw#G1P4|6E?U6X$Vx2)w9Bp5P5=P;D zM6|)L`}f>`i1CWVwu%Tae(;O$-y!e{BHQbN?}yS0FKxPTkFSb#^W5gLY>10xnTW_S(OUSGmJ!*!=pQ+CxHJbuuC` zXKLmaSlv)@njW61!9GNZ5OzXj?1}3PKBcH z!Kn^ud1+bfctP0P?bc)c{v0p8sbhS4##KYrcBA>Q8VpO6YWO#d-Xb+yw;`q~1|>D= z!eB{NluFdd)e-7pnr2{ZKC?H9Jd2kSm&gY$@ru{tn#C3x!xz6mmrPt9_`a<_;aKEj z{I@ga#mj@6c=8k6@h!mkf)z^L~|%yPY-aA{V9vxi==+6V6v*wAeb zf$7Z+QP|_&7l!u21sCD9ZX^!fr=M0hmt-84u&UL0ENl+Amn@2UZBCw~Fo<$p8(8q0 zZ+#Nz+6_Dn!ct{Vt?X$dJjc6I&7hZ5gHWlBSmI{L|v&L?{7xOlbtH0If{vzBv_J4(YDPA% z32Qv#S+nVQh^!$vu&wJkWjpIc7x1^e)pWc6G7vs{`wfpS+N%4FV-76<-(BPHxePqiD&$Xm+%qA2S7^a4TXnF-39nK5X7JpRA3#-jH=( zi#N_VxWx2KHFkEFqCxF6tb?uB7RURb^+=UVx)p3_1^v{+NWRsjA5YLcG)TRyf*x-^ z((BCqve0U#ecr2#kPRq`KGPH|+Gbm7wvVjPpm8@^n1%jJF7uiw_T6Yx{QZw2T$x!r*4;v5&}8%eJr+L z)xC`rBw(p`9aC&@lm9(9Hs8PUy`KPS&RfOz(%#+9xtaUqEq;<8(zPfp2}G9IFD0@3 zbtML@^Nu(F0KEed|#R0pQfs3>r-qNt)XRTd|)Q#04JF?Id$v&TKizL5WwDO_bv zM1G}=Vl9f6>RX5De$|>AEA`#3idM_IMqWolGhU2{4)&|XO~?6(yDA|a=VMF{g>SNI z?XTGu-uM8pPAdSPdtt$1o+3ha+b`Ah68_TC94{1-aLfHg=w4!CGm# z)mJgjlnR|So?RCZ1JoJsJVL!U@LfeWE=4=+vQeU@7u4(qOm}_Jm=m+}nw&{R0YYQZ z(eK6tGO2FWp7fr&SNg)rt4XL7H06{d>wzt*|@GG%R4HB`Nz zYKWg+oCsZ%rZShTB_KhC2hC(k*1$Pyy*|4R>Wf>4$HY6$G#!Sik_|A>rwsKoXRMea z*R6VSU1D|}4xSoQD#t)If%>9b+K_+Fp#K!7`JCo95(jcWZSOfD!-1JAb!_)4nb6pk zB-QpGOGXFZ>E5j)$s=Y{oC_}Lo1}0P_Q4Z{l-BSL#*b$YZT_7-J0QJ`!7jENxr&Ql zp%Riqs&i9ZqE<9v69`vr8y?}Ksa#Qg)VRlCpr4!ltQO9s%mES@WEYMNgr8Y1Jr;U} zzPV0+T(%CSPV`FI{CIHuE+TkOR}68& z7iJdPsp1jZ*}0`aL3r00l)S?A{S=s} zjbXg{4n<>Oj$PjT)aTYaz1M+vpFrae?!(q0c%~XdI5I97Ovm14moRuF@MTZnbMyS> z>rWY8`EhhJr_I(wS30y9VM427{S9lQoP8X+wE7qk+UeLv9vAZC$<1^VCctx)<=yOT zBEZEa1TEszI*5S|riwGMyVmpadU>z2m^a6DH-+F`5SEnzK5A>v%Otl6ClTR;i{ z*vA<7lv-MBoG6y`nH{y`IZ7lVsDc(ppIn`5@ASe;A2W{re&+2{Jh$Sf+i z(84>6G7nI?4aus&Bm5;MFI;+;@IW`E1Mq_4P8bx%^Yxac3RgF#QH=f zuzbrMS4V0B>yUDZ^j#O9vk7;0+FLC&=Z@SvJOoVAhU1h;Q(605APhC=PYRN*N7#W} z;R(lp%la8)CDNtkaJ)otO2U$`3{mD+NOQ} z7qeizU1~ynBdDJc>qm*Mzb?c#x5k$2_Su{)tK30 zR?BSL(9E{RpF8@Cs2`l=yIbrjbxU?3HDD7ut6DR!B7L{Ihp@!D+;G(fx|PAYI+^P} zN77Q3B^(w{;9BYHww{VYTW&O4U~3|7-Og~CcJ1uX1_<(&Vr{m>V5U=5fz9;T-|HdV z=vzj<)7PdnT_f>fg{}wIx&xAiI-(h~gn;(P3vX;ZNA}Qs$aaU(?bZ(kPMSAeyTE== ziNf-|knBx`C(Wu7-05fA2%nez&Y;2fR6sWc(Dvc%mYhq{^sfH~Eg$|AFrEZ@lS0fSf)F8nP>XiZ z!rU<-Bt^Lv>D()&$YLwKQbsx|2x9Se-fg!QF)vMrJGwmG!WBo1@qvL-uK460Am-KP zU55x4pL!;2TN52hw5q%SJ!{72;IDdRboipw~t6NO+Je z!MW-OS2xo-`87>%wv+03A26}M4cXK$<`HknO0uL(MFf!}24;UWhEofQ)vZh#HG)i#k zrB?UG@}=lPtf8f(K$uaPjOe|f%l98QClm=V<(s*C+9dn1JYs%Hbq$$68J~&$x+`Cl z{vsSs1TV*6dKesPZgTMaU~fC7?Y2OP^dxz--1&=O7Vz;-jVn|4D#bmFJh=DpKFbTW zp#JZdhO6O7StPm$-xa3#xSRaO5w)#=Q+^JbunJXF9a!rb4!!)D({p$Z)5$^a z+nN%bY@B)vau7mL>S`)3SW30lU)vMvf0AK@UDeLrGc3=k3YaEYpW{O+`A;5yxwqlW zFFS1vl-$fmsrsyJ3A5Lw93OKxEySe-2}9wg2;}++h(c!4OHD#^MHw+)GjaAFnLbi` z)V=?tQDdfPbWgZ3Y=W~y#Sfn+GtNob;IKlrqjgX=WAq2t^-ro*f9k}pF+_UbWSDb? zGGIj}g1g&!^qc>Y_q7N*P^6)RMzA9FZYmCpuOb2${n^VtQS~hW1oGZu~@wTtZhCFleD`h!)NE!_a=b?m(m@rMY^2Iq1_L3zGuI&3 zsTSZCMiC+`yaT)k)cdN_+Mxd?U7sV|yvq1Q1pHk!bc~QW;*@0WyDAYQeN0M<3k#e~ z+}Dg6f}@SV_|ZGV3?9q?u1YL64x9>R>vQQT{xg+-jaXr=d4Co8FQn#+`B0+ix28{oeE9V1?9GD)a9`8e98m4i|@JUh467A?`nA1!`mD=sltEN|`8r^CEy_ZntJpD821oH-O7 zr_u_0P036#>y8z4nl|Ci*e2z}?Bh84@#AsUzw083f>(5zp_+EA4K=<8l8`r~c)exB z^Fyhm;!I@sVdu$I^xRg@-*(0)wv#oQcxxMn2(G3vnJFsJO6?+`DHr&9_GqwTG{2%5 zEY>7Irw3i`_7j(%xpfp0@_3R$*PN_Pxc|DWtd63?9gT|bqr+r&CMb zIrF$lz7&B7dREwcKc7G4nlP2bpRI;pwLYS9jVmJ7HN;0BO7pM8^ecy)H~C3Hf-Ku6{`TOk_d zx<8K++rLP9^r7J1KYI;*+4^#UFMqCs;==zTED1}Ue{hubC9Z5l1@t>kKgmti8b;{W zu4!!oYx>yykUFs;is-2rh!ArUGgi26R+&5494x?@9)dkrLl$CT{E{cUV746#{B{L} z?+>h8&Tg_z*6O;i!5?=^m(%(E0Y~K2UTQB%XQTJ@aY{=ue-W~k8tii}e#S3DPpD_d zWEBgUBI~?bnea7uD1a)tiS*>gQfu=exf;fOy%u z2OEx|1%_GmP`b@4Zo=)JxzIIrP-I+=KhV}V4G9%n z;W5ZGvl&7R?H{|XlZu?d^S|43_oODP2^23Oo zO%dqr6)Bt<#H)kHty2N4mG>1i&^8+j!n0A#|HiEokVOEGJfg9|EnIIugUFnL#rcBni*jFp*Udkz1VA@iOHi)mGn!00r~$ z@6PYQYNA<*!kTH)w?ada&aGM>6X1R&wIfwg>R)ZMc0v5iQE_Iq?<*|Xy8rY$`1M_D zP$Bf3o%_l*3;a)}H!{gkP)$LPGjThiwb>{<$f01fjiuzwPEZ%^KiB#Vg9VY2(L;yT~gFjxFoMR zMp#aHp}cOMY2PfWMiP?UiQaK}!A$`6BDLL$U;Boyy`p zf3TduDqW7Ck*cUBG?Vr=K(fp3So=t zy3TSOLrQaw-ws}KdP;w#D_JO_QP)BVYa*Dm09h4X&UONq@ny<@wZQe5}gi>JCe*R?y(Wz>&CT~t!|2Z4MA zKI<(0a^P9}84E#;k!mKlFVL_UAG1;$k8j9*;XTa9^OPC2dy6fb6&~OutUjc-Bq182 z;j^MBdV7xSL}U_UQ3ZoHO|-@d8<|3~FyYXuaT%fJ>_?_SkuXzEwVGoj8nZ^X<6%fy zL`UrYx+0#rt=187Rgv?OkN8xNYlD|A8-V520HaN*f{`=G8U+UFQzTi5YVLZm5NDbZPxZoR)gw0D)ezBpw@dQ!5JYkFSNRgXe!HtBx2T>Io@;vQu8p5`daOd)bvW7AXALcXhv?C;%IeG*g3$Ue_5>;b~$yW>Rd8Bu+xYaty$Z;MZkEWqqv2@Axw8x_7h1QOYjw#e2(29>g&E zO6+aFyAnPUE^keNx^`4TcgJB18W<7BKGue)^0x0Hk}{bKbUo<>P)Ejj$JnyuCbvL8 z7f|hT5r#O=3EJ-0aGI_a3Wp}Cfa3L5o{w5^1%>*KL9U%xD2GNuP^>!fv;P) z=gK39=_hB#CNB*3)d;Jryq@qASlggvAx5W4+|+MS(+HSp@z7o=$g~*pe`YTC5C8;tq&oZIYehBJvvEI6z0Bm^j9&i8B4M)~vkR*e0|c>oikEKwF|tTG+5kf@iYC zF01+FWqgw85@R?w%+f196&o2rAh}IuD>a{z!_E~=jfl$4Vv=l4!d&KQmu2l8_lT?? z-LR8#1kgRU2ZeXrhjf|NK-SefGJgT>?N_mQ(R?25A7BJmKF)JSZnf8^<=(L!>||uu zA=3njyKT2e7Yw|yJNJls`m@m7hKg)2vOX&D1isvG_nT@fLj@sKbd{x8U$rPyTAZ>d z)=}+#iKg-0WkmmLYq`4g;E=qtAq7a2%duiBc4^}Gg9(Xhl(-xU1gTSNi}+DtK|FnzN;8A+ z$a44uK`;;_2)#)mxo_Q;G;%3+r!u{@HkNC}0^?os8W4?KdB<8>1>fXI%<2$ihN6{n>N{x>u@8N9D{uIHI$DL!!O71Z|L= zY4ge4ZwW`Lihp+aiD`$e)_tm_TdF#?S#75~429I$?Ct|zF?30_O(cMXDlwvxfMWTD z5tvJYN$D%feTq>S22^7d_0VnvYvN#VA7*560V9p0*(+10!s!vfgikPv!k|a2D5=`F zmb}ukgz0hOO+SKzbI~edYt+xLoI8jc{!~kB1%oXcqb+FV4&2*5co!5Whb8LxsrpmI zWn5%+r3=ffuIq4G3gj-S^eNp+_ZY2>RTrg>>cz%Fe6;o6d+yhP8os*7k#NoS!MBgT ztuAi{(G#BCq(b&`g5G&Ggvsu$uEt_JnVKmzAEQV-R##8hyD_;-o(;c)?MqbIb%3KJ zWvF1}RCne)HKN#>Z=2mF5od*DdwE*J6+}1rs{u=#H{X#vfDLR!-~uDSi7%%1xLlzf zW_F(zVuqjU;0ub}cI3&WHL;dV(|OyU$7Gw8P5N02LtwX{B8H0DO}u)qnrdODJzY8$7CGDnw#qX1Rdla)N#; zsl_JAV`olmJe>U?uKUm75YPjkS>{GqI zr*T~d=qX%js7*&6?5^=8bnt|pR@e7v@LRTMwe{udZkVpfo6%A*U&VPoP5U53(UaNj z@x= z0{5N*)p-Z!=I3X+beu^AR{ILHGYzd7BbOvpt3Vn#X3jEx+E! zn#&?1dh5W8>|NuJ*JP%^DaTMqfZXyguIko+grUC(z&&^XhvRK}@GbTo^1ac#rtu1W z@Fu5S>{(rD-zO)9=nQFotXtW~huoFUwEPv)lc?((~SF;IcL-`slwBUEs~xsN?UNgZCZn?aDa7j(G%sM zFKSDP;56q#BQHCAcoLIAOUuHyo|Vkbb;SB6=oiYYvUkIsp9Y(Z#yGR}D;C>#Ypi68 zU+muBRPUcAWw{~@d3+7>OGi7~teiA}1C@zk4#h}S|#g?XAUpMn(oH*%H?+p+mCFWDl0BiUPUmS4w$RUp*le5xPTZ+%2iLD{qcOV(Y@@_y*S4ap?)Xid|-DT~0 zaE$5N`;vdO@%oF`2wDt3n*&)hm$ouhG~r(Vcwe1^$dLNgvIh%##rLppt^2mbEYuE> zAY&oXH#tGe6M?FR99G$r{LQpo zmeGOAAtv)lGi>=N`VpaASu!KAOULU4cip$daY}q1CB6K+^o<%Z6j{- z4BsYm2AX26Y1Ecm!!-t`Q6ms-ny=v)aV@vr)+~`o=g@Bq;Z?ERPsiTwH>4IQAx0}A zl>Mc@xhYk;J=xlD5R6YJ%rPvCtNmx5fl>S!Z@Y4Zc6SEX#;O>xp)sNtCnnkNg56%# zCRH~b=`9yiz}Rg!(OaCXfd2a4XLT50Q{J~@$8YI@?h4+8sW+}GSipF6ztOj|nzJu0kjAJOUlO)rqrGDz}}NvSht;F83?}Q42JDpuubo3PFH-b1m}HN-V61 zyN3sL3a1#`63bz2U9v4mBCb;s5}M7mYBONH0*8C ze%NY*Vdxazz`R-n)FeFa^~QzH$8lpmnen|qVLE<2De~&Tc zHf26X?(Z}w{)~f?qv`L{%~M4_`yDYcZl?Z!w7q3iTW#NX3B`*ScXxM(QXGoAyC+z1 zZK1f+;_mM5#WlFQySv|$`+45~J2T&BK4h))DQo4NtdlF}+P}T?;@h`$sjV)|rNFU5 zs|49y6BvbP$F2J4Ti#@5XAGE5r-C%;0r^?(s1RKt$=1J1 zltgBw)+s4wAzYe12=T)%n&1*wR$BKONT$Xbk~bQ0NT5P-_G-Tz7y!fdpRP4^CD8R8 zIy$3m=>bbm9q4H#G?qG z?iyB#{P72LP=rVTylGG6=cS1hkc1g4QL68dGF&Oe1OtX3=0|us#iK@?R%wH#7u=c5qGu$k>$8_7$7wax_>oQ|`Q6H`5FXs;Ui! zv))|Ez4qO2HP6T-qsAwqfsw;|WyNy2Wac3^Jd`tq_=3l~nuVaM*Buvt8TvKefp}*P1oe zOrc>#`o(k4t?}4Vhm1#>L*kVJQ*V0^5wxTBwxWLqC59r#xk|Mqp^}XDZTW?X2G#S&KnJR8sF>nLPE9zi_e)T zDzTqBX~e9m<#f<<>33LWQzY3QepXiZZB^OWm*qxR{lNtNdD(zT%SZQh@CDoB6JG6X ziBc%1V{oJPmhDE@;fcqe35KG<;v@oTbTbpCX23kCk{%`i3kZ}MT+NrwdDl{XGX8XY zBU9;F<(Q@Rtw(J}cnki;ILd?6{>atpE-w487){^wR?EDo?$*zj7oj0|h*J>BSUj(< zI2IuycRSj|rd^^+hDM{a`b{LEvGbH`Q5rEU_dXD^aY#zstGDHVxYuC_k6F8Pz=(e$ zj}+yw^@vjW_(%IU3x{+oEu{!+yZKw0rs*_iI)3!LO0zhXvJHCLOBQO=6FlR>1qWh& z+)F~nFQ3IRhK@M1szTpH9CjIUTS|6E(6sM@&8D)@uHVW7f6$ob5VOMJ<%GW zOTmz#pucx^^{$VFjjMxwnkR00aKc-%c=V1d{GHo8Mt1KFWIT8@L`0kl&Y-srvtL$7 zW+gxV5F%^n^E&Mkl6)h})5dCkMV5-4R^Z|O9*SYoZZr*eA8;jNi4kw$=f)3aX8|O` z6-UbM;MDE#gnOpkSZ;Hk8djnY-MikX&5gmHvn281wvbA)C^X!3%8nW~l&wc15iY)J z<5q_r6Z0Ca;<$JY=222UVm9GKg<`QJ7LGGRd61UX-d%x}iGJFqzCllA{i=9qyV$vgtsH{p$R~&< z7B#+uzD~*&N58-k=GmMW`=o@@W`5OYB%yG^e&DDNkq8K!ZQD%mUlm)hR=Oi-%aZBs zola+{r82~6;E4_W%szNFBTzWR+nVm9f2;XNNdPR&D$>PDJi00(>-zi4>fY#I>0ns* zwlt7+?NrAtBu!BK+3ZPpJ_b-hQq&o$a*4)3N=$;D$+N?L4(0!gv=q@2dIP@QlV|*$ zIZ#9thr`)T!sEN+mv`*83TZ*ztYOgxCyKs0zevUo&8H0Q421oewb39=6t#)>*)YDX zAPy=H0vIl@_DPDP0GFF41nIX}+(0oMJM^DMJ#y6S0$ZOBLBye&RgB5sW;}bjG20D> zI^mOr=)dsW`9b!aw4U_z+fy$_2l>>z(i56`;6&`36WYNqK}YR35L5aV7}Y*@;|J>NHAsWOWgo}hch^K(MXGaDj6X>xBFK{Ev+eAA3J zXt@0CMwPM%ZAMh${1(p`zS==6dvC1-)NfZFgK^9Zs*^E_(7@nGNM#_j80Hd)*}XW( zQ=mGeqceS5;(nwgI?mf{chk_v)5hvsDwrg8y$_xkKaiYS(EsYX>xNl5LAZI`GO|@4 zQcSHJVVcE>9Cmhtb?vq!Y}4 zH)PYkr(%~S6V6pTMAb<)t(a%>3+ZGDX=f06g~liJ+hF6MJz{9pg-okWS>mKmwdXFO zm9TOk5>B0z%GFyHng=|eR>NZ=&Nx9=m9zpK3jm>$VU_vJ6+?=`z6G}-L4GrItL0C9 z;0j~Rq}_!nj50Q1I(D6zYTzOUF%)k%T(q>-P?n-$%5tZ$<^kT1om%?qH7~dT#?ao9@9=H?3u<4%FhD8c>i7fKPnw6Pi9&VMS@BS4@%zUmU=qg7UAkfCwRj%%yI3 zT4&WNW8OgiNJ6Q$w7Wnx1; zHh)@Cn2nlr*dB8Ny8cTCF})s_RwUy;xCnT{FG{o@13Ad)X!Js~YJh$R=$T99OJPV_ zt)MRqu+Cu{^4oKPgoQvL_L@~*&^Nk%Jz?ik_<=Y)(W~Kc=dTqY%Op!!K{_q5=lM{PpW;x>dg{1)!0aBz<_yjZ(RmeWf z@0ZulIXA66g$-wSW>b&t*-sZbs(fBl`LAHA!yBE;`uBQ}$F2I+i&bO$)oUnmwC(gz zWt>a~QrdEc%iu=OgOh5(Z=N5xCR?~~0~ZN+DEae_->@Z#O7GbmH*Ad>9P~&pzrRow zdygheizEzlBZ$x2W%E8iM-S6;{yJnck1bv-8^lV|%DVr-OCCc3%s>IhcaT&!r=jGc z;YjmdvDOk-6qynDNTZ{4tRS3pM6jX)sO@LGf{Y4{%?3)V6de z-sSkLZ32|i|AE^6w6zG@YkrnY=|q2iA~Kz8?L{BW9W^yF=F_(QI@kFiNeRc=f!=zuDKl%Ox^>kO^G2)_AkK!-i{MGI} zKYJVbn|y>@to3~Rs3q6ofV&t%=xi)P{txuKSnBL?7JogO+fDj=Y;GW{B^5P9@)p~R z5JeMjpW~8%KSNN-;iYPtmXIl-+6c2laLFWZy=sb2#-+LTcnv@>AAi8s>4o;Nq$Eyy zdUyO0jYi6f&8y?CYSS&vR;R)(*=Ntgx^yqA*!H@d>vQiPdvlFiWv!2p^5$_nN4kupTd@8WXC~@J=8vaY8b(PH=v>?!P}M5TUhZ z1ndyO+&)cAuGlaS8Gz8OA0*ldXYk3^HMUJ-Hqz44y|(zmaaiua`&6 z>@+c+0jtpWNzCMlW@SlFzX>`TGb(s7UQw={o(nc-P)lM2gN~b6hDJ!Xcw2jqUFr;v z8RVtVdj#`0&3XA=&+9SKi5nLYE;tnKWW-*vSl6<#XtpIRGMSQ+JSa@1DYx(px-Q1g zR}&^pZ)Xu1GZ(p1<7u58H!~6e7fV8JXA#^u zRsetbi>xF@$jh4CrPfZd@a43T!c8B&nFTXyl#ai>8RaTwK6_Z$Px~}l$e04$&|VYe zce|{J&t)F!BY#e2|K982yc>M-G}zRSRQArL92|-mNXq!#EM4Jo5E>R%=1$4daCb8& zmw<~W)Sf^>7;ikY#Ztq=nc#marYVr3NdzAkv?0e-rK>J*iYA9=@WvY5r$Z==^chD9 zh5&YUSkGHXC8JK-dn`*^N6`i(`6X*=y5ROgGf|^nt($qA<;V-CP#dCgOsF$sLuO5g?1}2F(E&kr8785n|Q=n!>$P26C@o>V2$mVBt3*>@Yu4% zled?+xb;eotWQgO>o1fd=A_K_Mm@h-XkG#NBak4cnTa}*`lGCS2U_N97>xPgP(uojU_D@{rE2i(xOI3~$wl>@hRxY^{Fa1q$LVgO#f`-)Mn;KT!l5`s;ZYU}Dha16I zw5Ar(Xvk&^3K`XTZcGQun#_gyW&s3GQHT9Um0h!@1bj<3LHp{$Df_kqVO4dY(LKD(@OxXmV0eX zqGofz>}1QkC^1#BT(c-b3u!JlbP^L(hFp991GWBw2iun(s-b;GD|o07vcNI3T`v^w z_+@g#_S`ejE7&;5^tV~NKs11rS!Tx5bYoifQny1-#=Ysvua#w$Aw0TkSye)Khoqo) z%&NFVG|B3P?Mj&B@&NnH43cPK76LFZtdp|*67C6Ci=pKxsCY1F@gS=HY<1r-v#M|W zVK{rzEa#ILp;v2nX(;yeI8@bXI=a}TSa_q0eSwBQI3aHyM?y+P=nJp@1_xXve4PV0 ztp4SW;}p#3;Hr5DZ9YZ#VaSzV-0S{A*itw06ZoB|`Mr+;58>(OV@H*HGGhEHTjMJg z&n5&HK5R(dIdGosM?WtwO=#0hUSy{x?trPY*v!dLiS&k*-nwtILwjjzXW9wgU z+Vstcnz!srfV+NdP~tp9xX99>xNcSw(3QBaw(#!po0wJkee+8e*(pbiHqYi;{i)$1 zdg8~+CrF3gbtvy2D1NKU@VnW4R<-o{+NIFl>LSvk!|nr|Ul>T&>woeBa;Sb*U7#n< zU|smlJD;67{R1Uu6i^SDK`^Myf9b54{0FLaJ9h&uCRX_z`cjWY#Cq{2%sTA&?6uW< zB~(*uPD5-aO-9yNj_#m=l7)ki9up^;6!|}SZCdqHVM{V>oZ+@n!&s1hR##s zCi=f3h^ z)n1uMYiNQIe4G$pmr6huMru#X;WD8<>6%{K{@_E~Jyzp77O`BL{ z#c8(I{~u zx?nV3eBk@;`bz3MAWA>rd$9)bJ}r8f=Bl$CgYA6=XRH^*7)YY=N?5w{%hhJwU);WY z<>lCc+(HFS66N2Hwz}pHPof8e_Od{Tch9;&6pYP#2tfPr((Hm9kv=Vw^rw#QhwrFX0!rV!lE?(u(czgMYWQ^%ny2Tu1v z5^U!?L74@RmUX#WQdUt?c! zS-(BwIH5wW!YInm4#7#M) z3G{Pq#^9t!poBNShQz#2Y8hAB|MgoU%(Kp}_elIN>?f&zd~{-`eB()z{D?1)idfCi zWQ>)zoM%CP8^3#l$2h?fYXO`zG+1bG9$iI;1m|6lr;vC*?YoOQ)^m&5(w8{BN&N$L zaTIFDe)3hCBy=_LZ0Yi%4b3GTp`Q`hnx7H`29JvD>#GC&T$8v)P^b#WVj zessT|;}M1l9ybk9xu~lC>l0lsZh=UyAnR9wOWSlTZ|NyfI!)OyJ`IgY6-QO>y6nN7 z0G>k3{}O+g*4S#_&uXQgNv3lWf?%~8Ud6&Ek7h*jE0P=!Dvlp`D<61pFb2g?*C4(} zSHtA93rv}}SB&;h%7^;x0pUw4o32r4q0lY1f1rFMiGMjmtWwc2K5g6v<)3{Ku(GG} zcS%R`Ob)R0E(kcaX2f1uF11L`&Wf0=op_ZkQIhq;#_rs<1^E_d#6`}{Dd;5eAO zGF1J`5MXUq6;T`Fv3fEBoo^^pD=q9k2FBi$omaHyTTE{KoNrqzz}AlWO$^z=pHK6* zndWT^Qdu#17C{)~vUD%M&@YOt~+i^mK4R5 z$UA%%BLh&lEaj3XrBS^pDLAf>;fRf8h@>Ao!4x zqB|PhQxW{Kj`4I0AGf(8*ZEK&{4QEC&r|#D@pPoQ#Sm3=a&d2?jjR+DhU)ccSU~zG zGMCUfI`nL(IdN0F#X0-6p?wph%iFD{H2%3baO5qir=DMv?RI6>{}JBbLwtl211aG- z&!PF5<(rIS&{O)l2W`HU)4@d=Rm!zfeRe=X&#G$ZY)w0A4lglaVaU;K3Z$~#c+)ne zs?5zgr^n^!-O$%0gq@Yg$4M)_8Yr0nB~!!j1?UF+j^^srhow6(*&f~Xx&0v>D=m+= zu5h}Z{SL}jbh-@LhL?MAv$8t8>fnIAecgv{%12DD(un++n7?6o`iihh%X@?g>9({0 z^l_V^+-Y~~5t*#Rb?G9$G!S+MMr!0?eY+bz+ri1}_h!*zH z0Lpa(Pl&bHHVkM*CnAJGiyvYJ_354e3!uz$&{;6emO#V;oUnybOf%SKYpIDHW64yP z2@^cT;`qYydLWp^+x9m>F&6zooRq=nIOhJs*~224D6t8N_)rk(t3MF#X)|Gy1|uHhyE@9b-HrN!O|!a+>d?hq{14 zK^Y==XrgmpP?^HfZQ6OR(az*d+z3K(GTcGKETg;m>5F}x<_Z11|3Kkl6Xk`-s3<{!6DHu=j@`Z_%PsFT zO?OVE@(03~TPS&ChIAk_5Us@+(Q_0)SNi+=2Y=km?HDS7oY5Con&w8Z4&7Cb`V`a0 zQMDG;2~2%*DYo~B(D)W!$otTpV~`36G4v21TNc%HLB5A$BxzGV9(x6x6pB(wCZIOy zjIIwx;@j>CV#gfbu@Tq>O+=^Og}}2J=#=XzQsnP3aq11yx8x#uXazoa_5CD@__5RD ztfI$!D*Iksc--;8PpCBlG0v{Fd`xb<(|WTG*PjXUL6MBa(%M2P2!R#x=NwG}a9M7k zQ1JJ)F2uD#5*OqaCzv#pOeNv{nDJR+MO?gEXK2mw<@(l3`=7Lr+Bh&1+g~^exTOha zH#f?ug0F}5EJwHdx!2)K3jSnNI$w6fnM$#@_fgsZz@u?n^n$c2{ znwWjWU=Sj($B~*4*~C1|Dj~MJ$6RX}RuzfW(F11e&8C2mi@(M#3qH6{1|6rdVKPD? zRY#vlYGpR$-nuGTu0#TvtT>X9==l{h!uMw?(TUD>dc*pO$ln?T1$CH)P)$&h ziO!Mn8V`S>xyHIbp;_wZ7WU4qVuz#lu<~n3C?1c+%$mR++D~90odOIo;o-hB?x(%1 z$a+in(P@58K}P`((*CcjE9cscM7~R<&7M|-!)E4Tw&3bm^t8{!Vph)Hkd>Q&sd)uR zel2L(&0#$yK>)^L_!JrA(DthPYyLFQia}fD)+Rj{50&~&?x^(SI!evDJ;3N`;E6jV zJ2=kuY`HZZyTa|e%1<*+ne-i5J=&hDkjCoD0pe%f{GX}$eM(GefStmsO$Fu~o&v)Q z+HI-gjP>c-N)xAMilNFeiVa_SQCG2v;}mFF*sTq0YpLe!%C@SNlOU+p(_v+(m~ z^SmY{n4yNL<|d>hC#824Km&`U+G>H6?f$1=N=BWlM`78UW3+raCQAL~@cAwnR4IeY z5;nm@USjULk_3dAIey{vObPwU-b_v2^K& zcIP^S8{xMWnpyq#EpdVe+pK!RXy<5?$Ou{Hi>p|5EeKp}+IRTLF zMvhuH>qOh3fJFR2D@tf#{-Nn=794YTuiXBWsK{YY`v=}2DN{~ocKjIsNen8GNK+d+op20?iFv3;cH!ok$UA373PI# zI%yy21h)S-r`JW;wf!OEXfIsF-`SIYblgb0QIAdW8jst}|LZDE>=f?eh3z)#qei?6 zqWEb-=+lM;^nL~@>v=e}-0!(Y)WvDnxaG*_M&CPl8{&B?{yfNAKI{mYKvhw&2!6IMb`~m2 zc_#bB0~hc$T9PUX5Y4h1~F zJ}g*cimhw(q#<8J52xt`AASGG*dD$^^wOlXYgMTjWwgi9+)xz3?6|Ym)Q{>PA4T@r-#24=#bRu3X6JZ-!62k(-^q;JBoOV+7N- zr*dD-8+8+Gl|acx^bNsv0j5$yd)teU42}Z5+jXqVV`T5n=8`YGjCxkmXZZ|lEgmg! z#K0Ijrgeh&IDV8LjmdByt7Wu*89nN<`lG08JJz>w5x7&$qa4P#H%M8Tm_7{}_~5T2 z%r6Khk!k&nKQro6;gHjD3n|FdQ5?_Q)|MQ}EptEN#D6z9d@R+d8T$Tg1zIMqjb^=+ zy}1$>Y|^p4RZoHT;YPD>t@7pn{P&nIaEPbpsBoTZVq?UZS8oVi7L}Unum@0_hPWHw zc*dzB*2rnt&op#%hA4s!Fn>sGv0U25+Wo8Fft#ikcZfU0WeiEqk!Il=sz`^$x8c zXxr`bkU7Jj^!Je`XvXg;4~sw!h8kWlnLiVxx81$bw4f=0bu&9Z;sK>lIUr-HsE}s* zvzpzz|MpT>0ZSKKT9-2JWQuUgpFW8hWM`NdgZAgDz2b9yARBwp#=I1AAs$PV?%L;=rP^q zj|H2popb%NkOfZOF6}m5W@Z?rWD$*}@j7*@NqT>Y#HJ~mpf96>ItEWE&jo&Al>*cyhywJ_6GJk7+@tXvKX|w{*KZ7D1y(w42aknPTaPW$pr{ zO0%wkAl$mYb!ytr8WUu7O`t23DMhSiIf~>7wfXnB2T-^yA4cO4z-NDHCXoTWk`#E0 zlM?poyXDFG>7$=j?q_M>#&PLk3Xwpm`*Fj$ChAiuV)`6bvTWRJ_b!j#NU~>B_Sb9Gxl#l`%ov z9+q)t<)3eHt{i8-7MM#b&|@CRWt`3b2D#C+pkXTF9^_;Ft0PDFzHmKNC09Q$iZPN& zoj$nlZ>FSgCyedw&ZQ-DA6p9*-`9!&qGCr8NUA91lo|f=xaLB+!k}X`eRjmwM<9vf z#)~qEKbAeuN4I=#ESi&`my|+E{D?M@_d<4A*HEx5zY;c?6{7MDEn(uUlJt(mO=+$N z4H3mt>wNAeWvy4j`VX@yNHI&U`a~QRy6EN#N8wEF#(IlgD6izK$1J#TD<)l^3a-N9eKC^(8}=k{u(H zg_^wr58Nnvu8LWw^g|S?_77 z1#?M38ea^>IE7qqQvmh=9Y8}vU3SNFpOm8@CpsB~O49ZZ#k*f2D%0q9mHTwvaMYKN!lWYvT8_B+N$JpOW zm+RD~jfLdFw+gMMX28j~)p-=+`&f)VqqU!+?X9c~Ru;dn)|kUS*F!=<}I-mN2?@x`aEIJn*Q?JhO-b2Co24?{!n;>?~q3RAx8W(ail>_six4@#WZmJ9P< z46-7445@Zwz*G$)KRmDG^qy=a_ARS?SehbGxlTw}a>oz(uXbG}dX(9nMrVA6;94}g zgrp}cJorE&JvyPBxGP>Tq<)k@~-RK43o1>PZ4%;W0j&s33 zqxd}OchSes2Hq4qM9TH^EuNb>GkVAIMDIja~GK>V+U`P7VI} zqt%=vp*h_XYP7~nJlvoX3NIGPfrS*Src?udmb)p!qKkekn3JT2mQP_efi^)GTe^Oz zElfyPk8@hBQ+m?Z?naqfMSu@FiUtjk&_Jv5fSX1;TNF=Bh@fvm_l!@L5LL=fP*PSo zS0oY`nVT;40-2Xx1LN2w@`d>~#5b<=o>&qVb)(jOaX>TBwLL&m$5OkY?Pq#d&-rk% zgUML$tsP=8A{#V*2E0t*x5MLA@;88Tr zJocb@`Wzlr0XsJ2m%|bO|0x9~BPgaEJ(l`AYg~7Vo<-j&ZR&xbd?@lwRN|^sqtghJ zJ`V0FSGD!X@`Fv;HEKz}X$i-31q$_p9PH*!nD)%pQgQkZoBeYVB#HubTZmSdD{Ppr zz79Hh8kF7txbmRF$eWLy1YA=a{0mVg`CjBrHt3{hTP=(da?kXB?2)kG4oy;DBzTvY zr66yrq5q{(8sy~POUi4eGTL^+ha!bdlt8sqRD`*mQ`)7fTb{u(5&WW-d08C7`^hr8 z|G=lyNH*~u7a>s9{p9+~*Ddg=I2|OvAw@dw2%Wk;_=t+;1^dd~wtw)mI+^<06sqFa zE$@=si3RrAnT#tmR?@^Nx5k#HN$xnC$Lc6s(jyYVhlpS9Wx-(DY_#(l8|H;IAJ$gl ziRhj>!@hYB9!hy<3>=j6^pok~$2<~*^IrwlKq@M0%ER9QsGkPQ0Zg0UjZs{$YY$nY zsW&%TN5dk|nbz?8#)@kJ_PYSU&**UjR^+yuX}Ajf4>lI&2^<1(SObn6bYN1*4o*xF zsEp~vfD4%?mhxl&i<)^cCCH;OZ~PbpwvE03a#uUQ4C%8uiMLgMNxcBD#P|F-;lgq$ zKYX9%~pI z8J?BWd{|l5mfc1)u%mo~x6C)}fzzgcZUvdszt5_TjV|y6zE>hOl`n`i++8=(E~O72 zk_tLKmCUUt%L#WDTT;Vpf?@MXcM>6ij0@O(VZX8}?Jk*QSvlujx5hewX?cYh6N#ix z9xwgE{uHjHF<_39M_mWU8*dp%*xn1~@!Q5(6yWQ?eX9eEm$80@WennO6ah$dIZ_!p zs}Poc2h<}>$lr3Mmd9Ld!_h1}aVNLG(Sb31W#N`d&Z85Bjzu1}t4VQ$Z2lHE;!VI+N|0jKQCQ6$w&#xT z1X{v_Fx25zf$HEw>)Vy`aFlzD>)}JJFkNCtlH-ZG&kEuC$pR;qW^-t~GfCA9h{hVvpbgA7>TeZ2O z4nvxxcdktVGY`PRj~*V;=Z-EF>ifWwjf<)pks(c_zzdj8 z_|a&Zn!Qi!M8D=HzhIE1KwWfrmfXYN3RsAxWj)3Y=X{$POFa;Is5OSjWD8HyE#<9iT&jz-wb!3p>^!Bnqk` zFBayyw+7t_Sa<$-P5jr>cRJ1?d}Q4;=hr5WEy)3VElaVz{GGg`Zv=FhC?ax@CDG8kV!CO~HfiEtuX z3qV)rFN#_;8VYMXW2uYISDy}-+5zP6aC<`76(ng<4i)Mg>*kJRj~1kJH#7>UFtwef zmB6ju(hRC@z80d@uHMbT*2ZVE#|N3iN z%Ec+)`W+0@h9#|G)0jq9GAv`BPV3cr|JN_OucIA*ZtVMp0*ti-IvoTyTqZq~^$%Kt zEv4zg<}~N|N7Pj*;t$Ki_qw`O^!LU_hJy?EY-&wCFpp)5y653HJGA-s%&n>VYfGJ@ zB$%Amm>mrcUueZkx}9pBuSm3onqOsTXU|y_omsKBE{>P{bOz4P1%Gd7Sj22Ho5u3C zPtK5-XN?i>XA8Q=R+QF^5=NjVm*C z`wpI7R*Lb6tTdfMhsz(a!cnXz63}hZSp|4lsMTDL_3m~KiLb9|k2b{;lnh)8R>-$5 zmobv1t%ZIS`cd=1O7{=cD7g4ReOIh^_#de2n$!1(Cz+eF^5i+kMoFUtIqGYsf!1Hz zDcD}N8y^mCoj+BpB9T5!@tSWLE9&>k#}0}pDaXhElO1Q#RK2tY067k=BSUC z`u$F)Up4>)U}C~n@T8tB`=g#S4X6e|5Rrh3xB0f^K;a}n{XSw?mBxSGfRR}$m2uMn zp`lt+n(b}7yY-ea<9e@h;EO}bbrx{wY%-_PQiAYZ!5<7-=+p69U{X74JRb?UWV zO`Eu!?1Fxio7bh)tJHs2M zDm^CKbj|NinUg<{ysNBubYtnLUx@XXvu1``t8hVzVLD~2so@qjHSxQD6L&KXy_M+t zD^$7RPW24776J_ve-JN57A^sVH5R+d#NE|@LO>?Zt4IIHsN^WObop_eLzJECb7cmtZDlK6LCtjw<^5qy&33Y? zB7O+Gw1}!Dcoun168+PaC{w9^y|S#QfOP?4Sc%GiPqD8%6aGH&E8Sidjz2e>)YkT` z;ch1LCGoK(4|fsLZeoX?Lp#3(pq5vfsIhn3>0}eOUrJz2$-i_+;!*jiX~9)DEn~Vu zQ1Yt-D?epz6y#PZwY#>!-MR-KEhijjS5X4w;oA2o<0eriZy9RooB&0yDUPULd*AA^ z>ReD*qutJHGoA}JZ5J~;Kwvfd++|_+V%UMw+RuD`zG$tsf+8I&K4HfB^yRjUB#I55 ze%T>r002(~)wD<6V*DIwUwfD^VK<}n%}^+s!YQeAQ=TB><5tU0`>S!8x~X@)WyHo< zPz&iRo6tQeQA4qEX3we`$JrMI)CoGW8Bd>{>AFC!9{~YW4SP2ozFuY<9?h!_?<{A@ zwZ0%@zU8wG(Z+|qnDXB5`YcVW0uBo}8Y?d*Uea!}PtcF0@e|ALw`R`Pr=$#_h?Z~4 z!v`%80^6l6#0FwTv%a>~x8Zmb{4QGARP&|5XsBx&{J<(dnZIn9m1chbDi+h0ATeb< z?w1DDq&4eTT=8yZ7VxaSGh`FtNJ4eY0rAQX`lESG35wBqHd1v8GMn*{*^~A?GYNLO zpN)NhU?B{|yB8S^6=~tEyO&%+Q)OvjX?_mp?`PTkk*XQf$wON3?~6RoXO_fnZoLf- zF~S91+Kx8uUh{3!<_(sQ@gjN+q2k-aPBE&0m;`MiPaI{5qemDk|Wx@`O1@bnJB@CutPR zEH)lsIZ8A-_bD@=ZaZkVRM(H8{>kBwM*A@W0=)67(&AdnVe6zbXw6`^|F?@4y`Xnz zO+~^^8h$Q`G9z{@j_%X=A4|g)5s= z5x{s4pZoP5(W|;MFWV7Hhwf-T{M*YVTDLr06$lOBE5<=5?&GrGxlBOQN{T2ef#5{N zNk$J1@t2Udpb1kn=RJY{pfh+4{H@u<6`z!l#2*S}1oDr_swV2UtD-4~oURH1nI43% zyr9pyc>U!UGUX|pA7J^%V_z8bnTX%s*F8Pie0D7}a(uM=o8RY5yAJT;oL3B8AQCyUEeSR$Tw_)f`|QoNL!BOUWZ4 zM2;OnVD5zaZCYv=JMAM9`VW*)+2+68*azx78YI(0x#R{nd|wx0yjm(gskYS;t=X)R zu=KyERjFUJrlcxbG=ykw&;&YAu=G>XBWydEU%%ssXi;cu2Jn|Aef7d4@@XMI5sg@W z6MNn_dWLjtFRvU*UuA+-0@+WHTfBR6lOL`P?8uK!{tv7bk2>xzmRD5P&%{^_fwhLw zDR&(`;BfxcpZ-TRhTR6>Z7PCfbF)Kar0{c##e zm>GeLTZ+iR>DnrX#DD!}Z*k6N<^@gwASlBN0va&_5U@(_7^rPd>CA7nV^>x@33bK4 z{#Sgat#NKd!_w~%gZV@dFmXhq4hh)amzG9QOy{(`{mIU`5@b7UTpdDV;e>{;9Y#f6 zRYf1p>fAqWK;X_%MzpbGU{>yjtxTuW);7rp>%2|LD#LIi3$t^K)E;Cf5)uY(RGQjx z!-Fp!x827_eTvq!s2ky`SD=Edt^db|2a(9>=M-h-tRPza`GEiZOgCrI&Thx#4VCrnEa(qJkca$Z8gdz{CbkEPzKTNdKV@tB?sPTBSgQ8! zfnfo9lOzwzKn7|N{Fwk6OXd2Of|m$wcDJ~YH?*=1U>!(5DIq)xCd9*di{-g&CAwsQ zwo5k^Bn7NDUR3l^=DcHA$0CqtPe8{(}74So%`sELg zo;hzRO`)sq_pULC5bxF8D)lc{r}fH!Wie!1AK6&Y@d~Z^`O+f}4Gpv0= zS5wRvo=Mqke4FaTS*=N`=&`H-o*d*jT=h>hp28rUJ^yrHZTGCYi(4)()?$=p@3!&J z-_`gAS3E~rgaQ1CrT4&GSB$>~2k?Q1@z%&EU%Nk{i!zE5Y3p2*l8{)`swtj{PggU8FT{ABpQp4c>Ko(Ia`~+-L0wql=<+}2xd!U)+t-^_A35z znPj8p)`>9{CswiAY3zvIjTDSp+M_C!`)N(iqvoX6&ML+!`04)951%XdW~m4f943&F z_=EQY$;nal2aAwuqvkGtH724t3Pn!H+NM%@y8W_`)~4?E<3)BsIb??BWRq(=kk|ec z@|zN;zO`(_V7@W0Dc$QzlCIyiSLL0L+iy2ULIdatZg$tUB=lr#3(ph|o^%!t1~0hN zS+*>%h&7f03S1!}xiw_027DHVP9CZPr>(P~TiVkIeacv#wR3#_KO}qlI>)IUfzeU+ z;w%%VYj-tuh3^&pCq!I^OjNdb_4wS7Sf2=F3d-J(YV6PdMcZF?wb`|8xG;rMXmNKa z1=`{soZ`XVwGceGyS6yN-5r9vySuvw_aepHu3Y!~y#HXW56Q^KBx8;-&pD5M-!{z1 zi5!EB5WDf9_h0ZU;-*6CpjMy!(K=Qo*iy+)D9VBPUP(VbPFhl&Agz9*qpy1pOqc0x z<9f*bPuLC9e3Uvnqm(}0?x>5WmVFVPN{HRZcZV~>TQ(AL*FUE1P7+$q4Dq`&E3?%# zMjcWTwrIngxekz)jw`9XKfAC*^nKr17JS~$|8O21qvPtnNyT-Wl09AlddUo0*bxtN zz1pf3Z5g0M%jY?rA7ExQmG_QbEo!|}m56=b;Wz3Yhg*m}GM_!|&6~xY;J3A5?6r|C zJaddgO+caD!>5ezo{r>%xNM^WPw+|#wznKppXy<^k{%#`^R#D@eBl!Ekql>ndoA)c zNxu$}gHsAC;y&4SA|TG~7M1oA3Fb_;>TtnDiPBT51WL8p5lejFkI%o(oTZ$%FQqnp zU=wVxoG-;(Y@eA4cK#}{FSx=cg!mxVU^;C*je;aI>oS{yXf9~%ap zyK>xi_}3@_yU=;I_;j%uMYU2CWw9}!)Z?eAjS>UB=Z|Zn`LVk9mrjY)kjjkado#-5 zVPP0FiZ-$)6mrQpBtr|VgKg~-zJq813^=8iPEqi-8php3hW;T)&mm3k6a1a}kSS#Z z4fcY~Z5=*5AiLjbqWus|KZ*9aoOhXDvAPoUV>tJ)^;jR$5Z}r^Z?skxpN`Ulmv)b84M)NUbMpNoRQe?yugt!#KUL6iAw<>4XIb}H= zOG6HS(bC&=wXlfbpP247vP$-n5Pji}Zep92KjPx+jJID-X%vC>IDS1~DeM0N;Tw;HJ;r;Qz;Zx#mPWP9o5$qfX@#|=7(&3P8h^zb_W_}3h{g+oU9*1 z*xCDmx4+P8oA(XE(*7%vwp#>0RC&TYgf#XDefs-mci_^lG0U{L22xIaqhj)(GWtT- zLQ1r>-KXYZ{?|<)PVp*@QbKY8>Rum3#w2FUtd(7nV@U{M)Rq+ow*Yrbo zt9*7)u`a*mv2naxNc@r19Nc>nCsY5}sZZTydYd9AE$(ukQA&Od3ckUqJTH+R8b;e> zMQg0I=NxoR6d+MQFS=E0oFqaXh~K&-EnKHoP#nqx;_ZSg>)HPRFWi~zzKG?^bF%K#w*Xc21{W;~9n|j5wcLZe5XbK~ zN7?eQsv0kb)h{@0;V_m=(gTyh%uINEzGZ8!v!n^Z zIb3*?h)Pt0S-v&RW1Npu1lCbVpt5+yU_frZpS45um0!ng7$e`iE2=+p8G|*c;nc3G3J&Lk{rNlyzQz?(dFqe04)N)(8YLK63qvdk3D9P2ToAb$yKL~%U|M* zJ&7AWG_39pi*E&cTX0>vI1G(bU@8j#WuQ~%wsT)P9Pz-*WwbSrNUI;1G4^X}s_bUG z)+;TDyH4So&^=IE6TsdMU!*8LDd)PCH9V_gqv%4 z?kjTfrT9n><7Mpg_qKx7okuCy=myLM-vt{z`KZf$^h~<9YP_r{|58k%gX3(&YEy7~ z&rG1`yhYwicFRSfuXGr!fXAc3-g;$8REuPQ83``qP+tQGJ#Ur-=)NyQk!tO3TlbTl zo+wFzOgXi#)lLhNO}---td&|CYy%lps}}UUb3DQmtXjMwvqODHdZ%)2OjC9o728*$|Gyb(Z(dk7=ZgVoUbjoE8AlsjHOJFDg zp)6D7Ulm5Otmb-!$>9f<@q4?hNAEPFu#RmbnTiFE?f6Iqh580Zv-rHa5dM4TjmvLs z!I{1LL0EiD$a`+S)C2ICH=N@F1(m0JkykN6ZR@gZc1sy<1cBx!U@{DDHkRcdA{%iF z8PF%UK7KwqD@*ca_BGsJ>o^;TR}@dognGfOW*+U5m}KFA8qSF#J2CCO_NL56#?R-M zY#Wo#;%HN?`)~Dn3|uPK!mnjL%5x#8qv!HWGVj)$^(XCSU~}U%`(;dmK*yFHB6 zIu*~P9CKE-ZHm{Yy1lh^>T_M$#U2IcXK_XSMr15*C7@sD{1(74n5MHoo{qoof}7UT zf<5{o=Xi1JKCR=G^0lt?VQ7uB^tMrZJalnUvK;ESXGZVKCo$mnrTxz)p`-0PWRdd) zb;W5gO4CQIj*Kb>DSjTp4s5YO<)V$z1H`-nW^Tb~@_KpykN70_x1{kfK0WNy^@MB2 zL?`KW zgtT0$s!j|vU06!u%L3=@vZX{5t@_4$$w+7llBylM;rB_(9sS3HX7UPKO+-Wpv?+D7 zd(Y+iK#Zzh#;sNQ@zF&He`7yM*Zle8qAVtyn>p?1&f-OAnwTe$$evuyG=hs6l z@K14Y#vE3$+kMtWyz~?V0dqMhM%AoB;FhL`KUU_E)%t51S^y_eV`%WZ!iv4}=x!}; z_$t#%7JsgoKsWlBoEH6Qo^3Vvmb`kaaT$fwGM&Zr^Yj(XtKLe9aYN0LFi>I`pq7U# zAm+7cL0by|s?}u*>zQq}-P3yes4$$`T&oFHerzq_-aKxuZVpN|QqYoLGF&u0e)#3u z_ObvFb`;k6VFQ~Ze-}ww_SPQVrJ0Gao)nRq?&XSaQ?qtecSd3OZA5$ty^pPT!F1|+ zcIJoFwawqOY*R{BL37eD8-9Yugf3FIdB6oGUnXDN4CPPUU~WE;NH*X-metS>f~MmP z@DNvq8HBNg3RWzVfP?MqU8XPasx6PV7j277EwIvcWW2=(H!e_W-$He@PuCg4n8BpZ z=7cESgA%2c#nJ68oTlWda3(pdkzCdaX>D1Tu(@tZPVW<>xnYQS`|M!BV->q+@(6Q0 zY2W3GBtK~wy;ac#@A*~&D zki7jac;U#Vzdch-e8 zz-ejmn6^nA^J=xzaf8zr@9^@jb&Tiip+ki!6uL@O$TJJ)lW69j>v0jeOcIw`&8Vh;Hv5XZfB!&0XAs%87lTseOy69RrCA`n)v$8bx&NyqVL_A9h z9K3XK+BdZ!8uwLl3Ty{|Au;o=&q9-|3T}?Hi$p@AAJVh<1T>%DD4c9?)mW%~Dk}m- zI*-g#=D#vmmB)a}GG&s{Qd-#CR$>JSp{K1`x<_+-fwi$q?+{Rtl;Saie7x~oQGbjq zg?5~-a~Lu7eV_c9lTb#8Qq@P=M zy=k3kJ^6m#b&q1#6k3I1ly0_tK8kAsG)0h0xy|!aT#QdngLCLOLbOIDS-@r?Qo#T z*Xvhm^UJ|UcGY~YUsSd#*mbq(*;R>$+3V3^QdtNL#k%heVRlQCU_=kS-&zgK1Rh$c zBxH21yptqf!d}W1Ne;t@FKu@%vg<>2 z7V>$zZaGUE`#Ox+^KjMFd9b4o*)TFuIOoQ@`)a?I?_%1U-7gkN^q+T2|B>RHuN@Uu z_feb05#P}JE+8Jhqh`;xR`%)bMh)e7huAYE(E$VsXl#i_chZYYwdszwaZ;a;|; zqxHxp#ao*~tKT72MuO#K=~a2Ri?UDVp6%JUPMV1Z5|(!!et1|Iq$X8COsnWIcQoD| zq_Qig{K++)Gtx^Cy1wmn)(lPlF{U{Hapr@AW!sY-pOAvBOT8S~+EHfZTL2~ zle8I0yj?2~41uAuEmvid`o(4(^@ZBJg85cQq!JAEp=rn;&~IWG^C~FkvoOalR2l;4 znL^T{EV6{~NdH+m;OBK76uSVxzk5wJTbCzllWShkxHVbuJ(j z_JH}Jk%c8B6dPx_J*b>9%ZYRt{FhtnZpDxqlH9K*qN{DQ97|?i{$Vo@I>{7-Dj!86 zj^Oz82u;55`>JpJ^STe7LXaNDD-jO+tZ|-XnG~HEMv7uq8Navdnsa`B9yLgyz{pyV zN$l)lB&nd%&=Qa=>uJ63)+&R-T7%O+sIHM7F6zx-k<_vF5HE(AwoW0t-d5b&Uxx7J zn$9l2$ARDEC{qbwNIlPm<$xAe93(^Gn2Kr2XhAdpDq;EVu+X?!PFd24eP)C?uG}*$ zJuy5-ICVZ;))XmT8%0SNNI_FPvC$J2NYr|&MXjfU1f?+N>+Os^Y-zebgdcOm_ck(< zEnz)=f+{Te%#|e$4IY-@RwBY0DhEcLwIOL3nfwy3;{`5Cv*eWGn3n7wr+6WMFkZJn z<*YJ)ZEouh7a6ATmh7y*L@bJ$6O{d;YG_l1))wk^PDverWuDGZ zA$c=Xqo(G#M-#ZXx%GkTDxN-vop#UMt$=un0^zJ2vM7!ubJpO`jXrA1NI{%Qnkv#62W;`kwNI=&}=lg=4eCmEGNN|zK2;@kd19-88)T$q=fwFCvF%{DER z+1j|$Ht`^_?VRvp9<&8Pi__Ie-kCRRy_X!W&qsX>#Hk}TSOt^a+a4;ml{t3y0FOw? z1K0~<_nhk3RhP90RrS^JO!QsuS=p^EdIeU!39wK$-`iX1R+_aJ94z2M@2w!^Rq$B&8&q^gGa(8hG~2MK;x?I zIVrW}SKwd7kYyqu=Z2j$5B%T$bc|$Zw7SkM*H|W4HL0@I2 zClb|SmT0|<2`leH)8s4gxDIuh)SG6yV1W9%N1x9BQl+;_pBr8vL-b}k)KsU78(V&} z+8G&xw?P=vIh8C!>1qkMJTB9}FD{y&dd*61-Ds^awYzagTdkPM!5~VsVa>`#Ti57} zvuI%%M|1UlP9)~q2o1@soQ*!!Dt4EWNqSlSvuusnXB{=nM8oKTS%y!#rJ%I=N7 z2!*RQV7Az}r~@%^S4l~hbeXTpvomY~ayu*G(wRGQZs3hH;=#;~IZ1!ah00S$Z=MF1 zLe#d5c_2r7d~%wZBNn1U4ty@HJnN2e&`W)ywE^;tA!qs8AYx!Vx5#x`919ZSz$41q zvrY;@o6O>7o|AaCJ$iD;{Bbp^BG2*qt1;$m7dBqGv${HXX~eZS=#}n0EO;7<&i{HRZ!Aqo%6~{Uqi8aS_bMHl)GUbq5-(Ak;o(gKVMybZGOo?nF2!5DY z5C-646&2{ZzA2Z4`D25Gr1IX*ll={K*5?M@A}vDSBHq*w>dtbQo7tH1(~wNgt4&&k zsST^@*;f!PFYeC1<-~E`I3lm?#%&@YqhQ01cJ>0s)LLiN;p*?DQi6iMJvI35=iVSH zNSJ11_TNe#@T?EN^xsOzbLanm7bnZ);3t72tfxW2>0a115>$R`Cd=aC>H`Asr>2SN z4MY^q4f+qmI6u(s(z@O4;CBQ%ajhURzqnN!fa0eDtXuoj*{NagZARGymEWmFcz^E~ zlokGX0a~@J%nGgQQ_9`W%zU5=UW>#Pj!3)8Fu%9Z$@agZ``yMhyMH)8S9tt^p((W< zC=E|3ebF=c^7N?6~95{4dXmzlVRP{o1!bX*cY_ySC?xd#aub{pDQl2W^zU@y6tg8n(0z zLWX4Qo=HE)okpS$ugFEr9!%rf8m>M?5#m3(pqGGw2)3zzzk3t7LhNK;EN<7@3w3?N zw7p_1-JCv^*VTz_Rx-6lFT%PAW9lE@=31sDes!~T8E`#0?No7zqrPEx(}&1ZqK)qP27yc-45)vidzipx2K?-YEp* z1eK3zzkyKUo96lq1&870+gY@%?sTE>8-LNd275|h?ac$w`aq1{mXylITnk3PYAc|? zivC!{q1b*&UC^r*B0S&YQTk#98Ex>E(F>Px7^SjI_thSXro0r|uP)tv)1@ewCKDz- zvtf^q zuh^y^)=Q-!GTxf>U}m%_?2)e zoq4%qSwA&)!+lQKF9S{PEDHDw-!iO;J1i{7knpwfJC!ze)iPk1P^iit+m1*aJxDwK z{uj0N?nOhtu;sRFnSi^NF22XpyhZ+ZXF4P^Dqi~+vcCM zcU7owP*MYE(3B9Ism!6c4Xe{HB7b*Z`z9$gw_GaPTqHDW{)ebS4GT^lhT*_4X@Yy) z$M?%MmgfC=L#vF^i!_upU)3tVOGsi4-q}V3NCry1fEhP#ubM}F4t#mcPzAmL?c0F& z5yL~%&yHtye}um+x;I; zP;FtCC|;fPu#wgiyoAWM>vkx_iNJzqu;h`pVa|pw}rbaGVz@X0JffEbFxe$TN zZpo4SQ)V{kW4Z0-%*V(e)Jq^=C6Ob7E1g46Tu48#_kmZ)Iwmf~$?1H%R+AxeM>ub$@Eq>_)mz>?>UIlxa;~}CT+Yld zb`DR{KBp(gw0OQ?#Pp~Ck?0#GU2X{FiVVdNMfPc?3chb`tj-Ylvv)iAoBW;s27LPE z;wRC9?iV{Z17e?#n(BuZ41ev}O>Al1@z!Jg9aOy5_IZ?YLuC*|put5v$VWkT61SHE zYagX2d)wf-hVGD}Jc5OUo;)J(8z<(B#V$F{cWHimc%5f>up%K*?HrWOjw zc=Uy9DZ;3}aVFH*>gRM*sGUKzeG4u^x_~;;T=HapA^z zVtnNw8oNlaDLDJmU;e8MC{FD0M%g$=dowO2qo(cAx6kp`F#3z^gong%8#C1(L*G3; ziD%G}&Od~6XzvT>x#RxmCi}gD|D%TdwpJZ}r+HOk&JIMtze}TuB6oGzcZ{LTA9@n= z%1}UX{+*cI-!nnsDz^C_LRqy~wtw@PIhkkIbdmaTSR+UEugR2($`HORS{1Qh`C}#`%w?R4{}4-VM(5F5tas5C8627FW7L*4-(7zuN>IeDk<(zkHEz(!6#70h z4p;TH)q~mJS5a^FZ1=6_cTf*k|@p!cYGpu z=+P-5al*KFE02;VDQ};d9@e=-y>KGZP#pb_^7jssWeg1%Hy$YM;HeY}Td9@gW;zTq z;@O^#*U;0l)B9lDvKCjh)cG;t=>sTKf<$P_J2D$~L`jPrHM|@eNTx#}mnMtOPSJYDx_*;7zodG-Ecb)so*o{#G3Ry@MWO&fM^(z>FRqNy z5)>GkOEBCGu}=Yhgd49KLK?mX8n1`>Iw(Ni^^Jm0h@aqzbws#*a|JC)_N9v=#z1qPNqc4uV;54yhv|1)QR2kdLboqZlH!Xvs3~upz zE|@!3kHO#jNd0wCTh0({M>z$wQ@ox@^~!@em5ioUeuK8;oZ0T zhG*7%oRwYh*Hy360k*mGngQKED$CB@@+?gJ%Yz4hm=$0xy7!8FswDi~AKVPObt{*D zF{7)L1Ck-^7{dGz#}g!oG{_Vp6ujXh6Bw%9evmEI5%uR`7-|X9&Vf{eX;s2;^2EYHQ|+94=(5`v+j|RCS2?IdiU23m zFyQm`iJc)sdOLX2@Ck#LjsG8l23}f6cLQPIrIAqdLCCI)9z~3JAgIO!caEd776oH` zvf2^6H5>#rcPo6V7Whn3xK78D^v9-UA?X~FUKwSBZkg2ETN%CMRwa!ReMh9NF}oxh zG#%MXpc0|XA%7#M=Z<|C5^uu?_JiiwuwOUuE-X$2s7lg>1y)bIK^*u+nuDB~0Sf;+ z@#ljnsPxKa>0%wCw;fvRt_OoR@$55M${j@ z7j?I3Ap1s<(SIVvV72Uv$EqDQb*n2pj}33u*h4+`W{`;}QoIcWLhtH`ym^tAZOoYI zXn0a1*G8?^^vDctSIK=J)HAFI2moA1%7m$8#25%0=1YGM%#A+RDp_BC85!31{bON^9-K7lYjrQhM4-+|AD#3T1dtP+!;@5u(V@X3l#5i1 z^=1g$`3W*)@B-@Cg)`>3NA6%W=YJxbV3DgukH>YSu#~31!J(ZN8uT7m-!Z@~fs57< z5xsZ=y^&i~cAqN0AFG`x$dwkfW})RuhTQoJ7UmzuQ@<3O29NxlV(;Fu6G17p3jZQf!_Y;??o~G;mR0xU-Mf zisc*;C!Jl|kUgvI_*BgAW~^;9BQ8T3-+^mD)43r8edVgkd^n*ZAhEZ+p%F)iZN+ZU>Zzt_ za=i~9(r5bzEv-?Wtc`hcReP!ikeQ&v%&KDPe=439I3)Ok*o&u}3$pw=Q#1-)}Cx8!{O&m%g-EEC;*A41h4kyRx-9jK9iVnL> z5LVEgAVvR9YtVy+A}Oj1NNrqni(qySclg--%57q=)e00A#M*Tx*K6qt31N52n9Dm&nucT`&a`eYct}D;5tqtWxH8)j(Fx!BPtQOT2 zM+?KQr1S)@XOOA59KDbPx~_N6(a3Z!ZN#c9jktbmpqnNmgribCdnY$f;`i~Q1-j|I zH#iBB4MHoznQyyfS?(RQz5N0xx+--x0i$qME246ns3%C$x3GdFgF~Q9$}+6E{dOHm zMHFh)aM?Bd5B)O@~g)$3Tw z>IS`?7R+KT_@$R$QtnLqJhC>?)bI^<%xNnauhyhnmO*LxV8Pyc%%yB{Yw-pT*h*gja;2%S zK5UrhPkfJl74P2I#*3!6K52SllAbMtWI#I*usif;@2?!cm)Dck=HUXKTPA`){nIM; zJRUTIQBv?tr64M~SPB#cqCwHs1TsSm;xH9nR84a28p1=!dZ$A1%2<;) zb$V5%)Xf|CHv8Zx(VDhX62zOqbmjbWGIcwB4GjA^;p0H^#xCE&9HknKrPv^;zpNm- z@i8Hb(f}31tg|rjnbUkHz->|*-0&GF5Oev1{8zP~lu)hM3aKJcw;J`IlP zsN%>^yJZ7nNRBQ}`lj2@T4+c->q-|)Q~qwAus_~J(fw1?f1F9CAZoj_m)_eo1{gXRzinKD8IykrHJ(u_v4ZHo4x0=lB3MnmUfXZDC!U2EeiXzvrnu|LI$zIOJPrD2G^cCMJB(g>GGFaZG?2`sc^eM+2;#;6qW;^xYUp)kWM_?W(T&^ z^!L`)6&T*HC3r_1>wU7%1Fi&-oDxQ9MFK7CK^9o?tcJt1C%eLwG3u`bP6r|MI^d`nqup7uqSb5tsTLe9hkNwNn5gd_Fd4$Y?SfZR^}H1e#XZS60}E$b73h zjW%u?KIw3&-S%FW<+u>3%u4_GNUBh8uP7TBQul?35-F%@^&&Hecwkk6&Xl8rJniS@ z4wrqSyrvnuP7-dKxG0@`CAgXli-X{j>Zd4)zv(EW3-Si*L|J0pAVHRZ_&zC*92_tI zSP|vNtWbE%%cIPmw>R%d)UvM`M{iXxVE?PefCW!dDV#qa0<{p=-c5(>n>oU?eLM-o zHGpd9(?1ihqJnfqNrQNUDt|_cbY792==-AzBAz*H=p4PD4Bt;c{n=z{Pm%k}WyIrb z!r@XQRd`}aoo!r@V_2R$?g_IX_gdAFiK838gk4nP_+-anv!=6>a6$i-Ag+iedu#CS zpxBdpcpEgn}}6k8tDUa^jzh->WDJaQhBc_c`-1!S3pEM-ir zZJG^e7{?#t%}u^!%cCFpJJ56e%1fWI&`Kho>}U@!H_PRt?d!&! z835+ImnTRU1*CGDf5ymL$I>hH=xB6Jez?VOToy7sK|jZ<9%qYBgFliA^so%JbRkJn zEgG*5gsQFt0Pd5HE)+P^$rQwN@5!56>@p#NC|qMCwInnS1*N$Bn@v*Ew63cAtNMz% zmAgfbpvI=}^B1iKNbJwUlGH&AHWyaDgB!jWy!F$3kq=w%mRJL&mB zVW7FXD7|?g#KlbBAI|5yq}*-e2oyA!3G`F>Tb><#$YZnu=a~)yv-0h*6O8>janRBn zt%tctz!p)R4;b&3AM_N|)#%LNIZ3D$6?Q9@^C*TO;=C4(kq=4)?BO)&xo%%=V!ba;3ZuBqx?ytXArO`-&C%eBY-(Bc=2-q}4d%2yyqKO9v+h*y*Fd@oGXP zr{R`o-tk4~b<+JlG10l%q3P7IrI2hSVix9vNQ94)b)63JAwQe1xQ9kU!Mgga{#sdB!$`z zirSEJx1msJGsgizc%Zk#^zEUdvnZ9I{8S4bCflopEOjr-Y5@x7%Oia*M}{FF2k};) zc;;?eHJtwNMr)RP66#&pxiSX}`aVE;E_1M3^|N@u#d20=V^{!AVc{|9LEm`B3aTp3 zFDqvQAekrpkV7nf)QJ$Zl z@zKSx^}4fF6e&<}F~o*HhLPg;z%y&?;k_9ZG)b84aGXL=9(!O3efbXcD0dkaZrkJF zsUyn7eZw9VFBN3(ghGdNx8jSBTlYDw!qs`I@S(vEs+Pq<9V*9!ODHu;vSN=b?6){E z&AEyX2_rOItscm@6}8(ih$=|`nu_)_bE&y*_7Z-NZ82aCH2_gt3L+uLr#4>1)=k9s ztj8HDP-WoWFd4G+&V0h9QvGh@n2=V5&^=&vV<YAzRfvx_cx;gT^rmdOdJagat8j%jSy6Jdmm*jzbf!C^@}^6_?vLUagGXlje-1k}7lGIw z-8rQVPk6Ciz^w%9>bp#NMeN4v3zN7A7z&b`jFZ2|W;klRr@f8}Rz{IkTpj~Cc%PXe ze{Wr6!!$K;&Ac;d-c5mHDk^I%=~!bB&_58byy~{t2h0(U zEFl6p=Wo@MZgz?_yS!?S_hzG_k7$4*WVeg*iU}2;KZE)EZ2i7;^?%7g1cM+_?S8*D z>PF`dpAq@6B4BymAiMj#LY_rSN91jSJEl+F7NwRQun#`dpNGf`52*$kVT~=yVav+O zhmlK`(RmX>sJgBfpmhF+KkJA7jub^8zsKi48`72WWxMC55Qp<*PGyu|j9S(PByw}T{G~b3*7W6V~54&v>`9&Vg+hQ_2tLkuq%8{CMdkgqan|9jm zBY-T2`A`{S#DnG9`50tUxnmEbWhV(qPVa3s^Ed2whfyAgn$x-Y7Hf-oEAZl<2So5| z$6{l>^wBh$dA$suc#Giyh>%m1MNhqk2rb~^gB7bi5m5}6Cm-l`>8vZJZ}0B3p6Yk- zesV7e=1c`+OtZCR3`t_k*+V07B$ckt&S$sd-{dl4H{fnkiY&c7XZR+T!SllA&-HAV z{jIjJxwSkr#meC@er2DdZ6K;Zto2+Ed`fx*e5+s)-dbnbpA)_FYdFcy>Q1q`$V3lB zfi8K&0f_5ut&)A3;yFMe7=WTHH+}txA{7$v+sv53t)@O^hCJ<#=iIAg8tYkGYNG>H zvY>A4_OL7gPC0g$PUGy)@qw@9+=L=HF@36_o2}FJOfwvjPO}njd>Y)m;frcl6lHY# zMt3R)=?sB!ucR9T2UpAKvof;mq!o!5bi-Nar2JL+ZBwzgV4a2&?agMa5@YKs{Q&ZX$WA4>QX0_EG#0O*VKEHHZEXpDFadI@sUo7QG>J42X7QRx>dr~ z5%JG90cm!+yGEOAz*0@HgY=A(;jlFEJ^N(R5x@eQ*)NAnR-U*~pG{&}F5TU_?=T1$ zkv-ON9WpM5WG%uP*V}}mK%gLhXfh{8VR%FELo(9koY(L7hAd?&i70DMw#5#)a|kVc zgqJ{ZqPbLZs<(n4M4qY{DTiebn^cDakb`Z<2Q3IBsuSNy>%N>0p9-bujTHopH&#`=l&Xc zbb_Al%IX6^4!idZ@`>E6Ns}b{e0(uPRQR%j4HsC<&dAS^P4JW8)|eRm4dO0iqcKgu zaWfETkl0j}Z9+o?Uc>BMR-xHW+x!^!CreH-v=@6@3;%kyZ)L;1Y5L>4?-gB16&9WY~kmLQ5RQWrWgpyCjzFv zi^}|swlX5~w2S=TL9F6zc0@77OK&y@hL- zWepRYiWF5B#a$WWtinpA+bhB;N3f}7o!qWp#q)Um!Mm&WW*kGpE{A@C67>ODtL(*QRQ72%=^4Sqh0_>WNb>2{8g(MMd7P2i(7}k#5LH15^T(=N|F}XNCv1*e2L7G7RSkVGR|Fe8|dYbGt)X4`|?7!F;xG$pJW zxb4=iMAz<|?%numB~x+osBjx(l*u>s+iR8G^k@?zg^X*URIG-FAfvu<_l$6 zIi(L%)l|7Sq&i>uFrm`mC>z^GM^>R82P0Wwx$gs?%*=73fLx{ql}zXDZ96BI`tRmv zwTubo=3rb?#h$alk@{P?x0?ICK6+~amC%U6bWbaRM+SZEVO($fM`Z6Gdvp!?Dfm$u zyVG56-y#LpJPD?y`X00$4!xKcl}JF*>1=?%j1h)b{C#Jt%%rkX^WTTD*fnOq`G<^F zC{b2BUTNU51=480XZ6cQD_YHzjI}5j5b-1WW-isf_$n&3mW~&+)I|Iu&-9L7_)&1d zY#5KCh~NDgFOqiP-u-OzL{pT^Dlb-`QF%9obyLO6x%d-+tg%B0p^$h&JsKC6Ha3N_ z3r&1$WgU!ph?RC&&m{!X!L#?@`-3>kG`I|Qu~+P3MCqL7acmoWck0VI;;VAaI70fK z)jxkz$@94GW0($^@0X06lu1u^1l0B34(~Axdq?lc7~CQ*9I4}wYJh8d8#CAJk?%;m zUfI>9Ds?P}$(xRt^AwkE2~t=L<2spu8t?jpYMaQ(TN@q-dm?40QbR><2eu5~-R6qL zM5*NmQ;tzI5SaXYnun$p7??4Na;5 zZpGi{N5B5T`zur~``$r3ErB}*T(ut9bFH$8w?0^8ChymPnHv){AaoAEq<-gkzw6D{ z+(3Bz?qt$xZkTg=)J&2OG*BnCiU2^|P^z_uMxe?-OzwFz+UZ-rQmcmwLq%q%Mffn~ zhHB%oSEsJpc(S5&SVum|n(WvguteF!xp8b!5LhtL?wTU;#c zLSc)huRkxDdrkw0RrrryXREBMk-VXNh&qG7?u1W z*6l9hxc?BoE^ip}ql$OVMEYCpT2}-M@>1}TezPfd?s!|(-Rm7Z-L_s$@3Huz&Q&Lc z<=gNN;Y2o4asD(XYJh7FpHfzL13Z?@2vL-RTP`(_f^9N=C#7D&W2joImJz9o*g6xRa||OmWn&LVi8k)`mwawqO~8U5(u0TY(-e8JRH@HhYC= zRTX!5NzYZCc5uVnItbRKSJaG9OO%iV!I@b26T;;V=hZJfKKNW}dI}cW5(Dy9EQ#16 zA2%q$i;{9$bi%<6(`_1=+%qHGZsN1!wj4_nOT%o5k%X;PchbVh5f_n zP*{63AIO2L{^xS6pOsmp@~R=@&e_APfLi8F7{+;oBdiSUm&_0CI?afHo)vhRbxI8U zfeF;_zbPH+-*?XjI~0GUuSXkCA5gw(*;H>BH8zyow)F};ra6^f6?h>4NQ%q=dYvt| z99`V0Epm^F+TQcML-}}&Vo*tN@!xyEhfd03{3Kz$yv1Es``}3*t8RxCXmD_$Ejn8( zHGdv3wZ&7!>YS1i?B)-Ulp2UI8!qM|kW8>y)pBxBj`ma~?T^e%TCmV2TLB^@iGgPc zV)hS9Y4Tmt3n*Bbj#C;?2h6rAUMeQb<_EYf>gkH5f=V^0`_8S&T${L@>4qCB2~5On zMZp_xmuMyPwo|hjwo{$BW%yGGb$_h`{0cn|#tYR?)(_g6cM~aS$~00`6G*`ZS~xf^ zhcJVf>SY>JDPCMna(eWU@G^JD+>+4>y|@BG2~=(9vCh89YmUS%#AMl{&UmZFY+XRa zEA%94YUT?oh7b#-|4){@!T>Qab$Cy1%7?L+$A55ym5)yU;9_?fI^y+f-E5V3et!K2 z*G^=y)$K?;KR#!aJ>7V66j(7;>II_BB_fbZbxQDRqmzSFYN=3DPYzR4W6VyQ#u^}h z?UZ`LTUN|>@!G2Y)NVj&-f*D#%^UA@>)bGfNPqXnGPCwY9ApS>0k6$+c}!)9Rt9PF zol`y#n^({C26P|?{q4}IyG_r3E+{~nKrf@~J8Q5DjJ_w={8WdNlwDqoF|75y5AIc& z4;6GrI(4RRs%R5kf7Qev0YB!z``%f(w0Mc_LQu?T3~xX~NGsCE(D6w~bGPB5WJtl| z^l8u3z$6T0i$OQdzIlx^+mH1!&K#FlxGG;;1^>EUir2 zi9Q(5b9AAX4s(Agw||47GUv3GVvC(tM~ph!0h34(1)~(3B_;Q{xEZkcAz_AcBkk}Z*!PqLN!w7l@Ml?Pdd{FaIk}e6WeB%kweBUZjb@hx0{|~g)|l{E(MKgs{_7U zi6hwu*I+7pOzM5Hq{)C|8yMw*V!c6>m?-#(_7`Amn9I!8tu1ShrEmi}@|vTsZ|u9? z5*w4e=S))GNLtGvg=B&ktcAY)kafP*;b|HDRatv1ix-!TV~9}D34X>*|F=c<2uogmS$A57z^_R zQ2UymG>l|SoLQu$h*SFy6(>n{B~S!ApDGvow^N{(-PT*Zl8=o=%PcCMero%e(LXnW z<}}8HYFln(X#z`E75D4^FAB&ysWPkP{Tp}aC5%?9yEZ9rl~ER1vx#@B5AiEo4n}yX z>&mP+@hdOniP5lDOaIf&DM)#QQ~Q!vtiMvzB-QNj5ngZr3Mo8&N)Bi3uHWSar|0qU&|-O2Quh3T9SbHd37>O%90v+N#K&q~ zTh~PTR{j_Btau=z$XhcG(IAo=Zj;Q&00k`pdt@V-vqD14R`fISy0;2rVpL>@5Gw>8 zE97D7Hm%L3*vsXz`3Zy5P)B z-rBd5LTV=FuZX-GbRn^JWTwSSG*bM%ru;KK0ZC4rlG=@F#;;4E1YDatw|OLv91+Ff zP2cjz9FSR0-bFe_+fpUH5>631veeKV2Zwcgtmuc=B-+8r6d zZRfBXc%}_tC3t9f4{J}%n*qp{G@8sQ0XXh&jCW3Bb73NJfm!546^S4I)MGw>Ep3(L zdc09f$^a?{jhhxZKXK>!fxh7BY_Gd!VzroqdoIB9>*VVjH4^fsO%?R*QG3?jgc0bL z%GNaN{R3FvaNiAODofLEc>g-5++<}TDb7H~-u%IxGM}nY)9pES##48}Le&(XpR<^5-ef{(J5XrBDnCW9uBt}TI>xx!)2vYBK z{O%S#KHO94k#WLe#`>$T8Vfmth6NbQy~KE$;r#*CvAI?vCC$uLB>!9f8RrPAnd_3r!ft!< zt>nQeKZVa+tZ_ybl1ex8A|KM{)Q7`@>F-*lcV=Wgia1mlufrdnP2wY6JwXF63C(@^ zKSGYRtLG;2G1tY2p%prM(u9}f$KO_{(Ct5!R1QxgBL%>G-hHgm!&F&^4~0kr_5o>G-Nue^~~MP<_&1(LvikhUiu*xHD@9r*n0OH ztrN(~LZo5bq|u7Djc3ONa2PymQ3@QijOPwHVF|Yw%qEP?&P>KRAi434q6u zGi*tRNx$q)xnC4b`t>x&XKK5zFCeczl9afM=-yvhUs+jsB_Ri_b9Bd$*}y~f9}V}Pu*=@n za$Q0;L7FkDyBz$O%?f93?P_awwprz~GQx!(k&?o|Y48U`6#Bx_)Rda={V3nEx%%LV z(Loi&GJE;+LI26lY5U6uV{UHmcY&77muIOlHSQ$xni_ISX5abA#+KJ0cj|7rtFCk0 znBaX1$4r-y16yWv=W3OifOD`3&K~gBei3!iIETR3Np}h~1Fet=8do1#3dU5B(8HU! z9!~m;K5!Z1DK-9~liEPWVJooK>VD}D6C8AUW^4%;QxO=99JSx!xf^PdUN2|@;OyO! zmg-RzOf8kwEh4%QU^~HGmKn8EI)0ik)s7A)?jGcFL?iTCO9Nv;Z!>2^y}|np(O3nx zn(^3+W0<2~sD9-#q>nXZ`nLr>);xLJ>diq#Jeo6EBIYLCTc(Svp9Pi!3^H{4`TDs; z*V~rr)?E`7X!oe2@BLvUJuT>8Q=dnSH0C3hO2sVpG9}mctY4>c zXu{_R0*ppj_l$KD)*NP8FOCQ=g#=$)m^Gcfgn(mOWzV?CENWlC0fNZO&r-P@Vab`L zr~-O|p^822J|5SdyW54|uMp@cZbVQud~$ML`S&3I;D|IYJhi3|nz(a=X|=wq#|!Fn zx_mX_S~PQBX^a2eK(u`3sR)6$?(Wsqr@av^^$89nw_qa!A75gq(LhXd{5tF;c(nLM5BHC^$WEE^Ut{?t^C?CBdOY zUAoaVr@o1_hOHebQzp5*AlZKX7nvTfW`JbQD_?~j3qGp2!Zm1P3ZwQ%dAGObq0~mr z&48+IIZJXkD@Ls%1>JA}W!6s-h?Y4NDcO__xn&Hs7d$pb{_=OGXY~4d!ev}^%*<91 ze5qPRf%DfSytO|QKe?^|;Ui{@g`g^3BSO0G38VFAls$+1JdWn9Lz2qoJ`1)^9e zgkWE^9{b~0O;O1*y7umV{2Arp7G<)S1>wlt=jEItIjoYEz1O&g(@`98DsSuW?R8Uu z>oR-#!Sf1eOjb-3Jr4!-?TI?*3OMj?=Ld&p`cCATshdvaHqP9C97h28djp26Xu~CI zNd5(v;$^HHU zh%rnI2NvFaRkANzwtC9YSuU2<%}<%Hmx#r!aAIjK^f8vk6&^>Eo9QP90}i;3(MS>J zRT@Yfd3fb>y={@Zu~+D>w#qH7b>s9KiO9Fy6=?Hq!o5bvfbzfS=I=k57MQA#m|C)a zCmep=%`_fQXyb$j1xD-^hGu`1Lij`@bkRJl?;`*hFK%L zH^p6TKqxfW(mz%w2>f$M_Ba7&atT))hE>!<*_vG8q}bj>M)1L1MI zW#IHahkFnD^vv)LN|lHd>^ zfqt4GJP6=E!xfA>W)ZGm)TIfp*^7`R65x;~6oPB{5Q4%)*~&_reU3N*sd{&8rf;Lh z%@Z=yrt?7h!(&B+Bvf7IJ-%bmFGA$n!g@3Hnxx!=)*Mq0Ecu;1Co)UO2BJzxI$qNH^!Iy%Sam$d6z30BGz9WuOSYNK@}tiMTQ-~eG8^JBmwQQCf~QNnF!QyXWFaxcZVpsV%G%MRx`ObC|S^U`9rf&mH+zqV~XJ%{S;gd7d{Be&Em+Vn$o}UVB1N9P- zR_g{Pw&;+$cX|i1Yttj)YQ<)MO0B8P#2NNmoiLy%ojxVsw26(eVoUDOtO-IGbS3(9&!^jzExUxEMq@Eq%7FSQ{8g6s_)0iGW;v~;GWehAM zEwwp!r7mljpp`*^Vrgj`siG-N2MnGYuzk(qmFjZ;2vhrH;{W>DqHO^0?$c6m==vi5 zYg6rUwH{Y9=!mB7s=5XZNl=Le@u?(bsX-$K+7jsrafwYk^!bpA7vB^xq1+}gJ>ArG zGZ^vl(c0cP6bVCJQ)-|+hfq?AL&mZ36In&6bi?F%(pijCnbjfE%of)l-swsGu0uQpBq7QH3G&84<*0+!%#W1y4ALFXJ=o1!f{K*fZBMM?F`gha*r zp8~O-Uiid3emESm@_?swAj;@G92@tJ-5w<%M4*yNoT|T0U``1)Kpz9-8j9_|K|fwZVPcjW$yySp_=O>mC~WReTU}oqU*E8@g>8+uXPGKK%W$B6kAt zJ=aZA9=`>$Bm^$Q;#SAhrXu+P>}J#iR04621SrjWf0QHey>O_d0f1!hl#Y!Y;;TU` zu&!i_-+JQ!p5?m1gSq!*WOZ6gouH?;cR2rFo6yYj@#8cu3ure z#kEgvHDJDJZeZXgUUu=hkf>L-Im%nDun6C=eloQU;yU;-USY{n$81!t!U1F0>Atu| z-ThAE_{+533fHfJ%>8*)|YqVZm+7xvW^b7u|MbJays- z2G`$pgn`(6F>N8af`B7TxtvjlUC#wDTlbiH0!Y2(dDZTebz3Hm2Z)#(oGD}6-7s4v z(+zzSoOCG9cUDD4N?qY)E>`<2uS!!jBTjdCk5F!RQn8+rYf{xVAZTHYj6VKjFK4`! zvYR@0LuJMuSbrOFV6YwRh|w+WKU$t1y|xy$KJOR*$k=4Vg)=j{xa~B9_8FHGZ_TK% zPo{Wzn20@xiHN8=YlT@CG7z(-TYpjb=ev`dyv?=C>VmWO`~r@ch`>fudSj9sM|G09*6Y)u~K*cmX$*lNX#F z!CHNk;~z9PqN?<0o&hpFt!v(W`G7ftxinbx(JN-#{G~s2k&EI?T9-WV^fXFgspNOo zeJe+3xVsh`J*ImkR(d6o-CZKuum&HAE>05-pWqm zR)v8o@|6^a9V!c~V%x6!_%U1zU2XW`bB!6{d9E7ss1N-siI`LOACGkFu|en=W`X;5 zzqPwN;RuK2v&u_@w@IaRqkN&FHIp9caB5L~i1_313!^AFen)Q265ikDeoewRgKbms zoWPf2^MbZs{mG;LRqC@`&tktN$e-WY?d;fFm6=cP#5>y-Ra8QYf3;r+ggOzDvUx-x z^*@9}826>0K%%dCo!O!(SSc!fsUShd1o3;Pz9rdXNN4`Q6v zRF#}DC<9(!7ODq{Nf+g_9p-lCg$YAQ-LIs{Wd`-0(k@B+eW7H_T^M%38Z)xMJ?B+@ zV0gDcjCxwt2fBvxdFwnPbRlB*RmP+_M4qvv?ZV`Gt*Chmp1vr`7L>`1tHO{Xn*evf zk~Gx(4D)~*cdOD`4~=aI4Vst^wVsKI89u5S?h*Q-Ezcdik0KP3A4Pl;O5Ww-1ww?> z);Vg<*3gU63fZzHAN;fwh$NV^Y z{Hu?Bxd!u#p(PgGuPl3CKTQTUYg!V$m4-J&&{{MQ?Xl`)t51sMW?6D@2dwS+#=f@R z>~NowrkKg>XK2SJKxwrU#($|k%brzNzd-Zc^%}=O?_j;$ZFzQ>VRdX1QA(HJUCkd} ztKYkp>-*u66c`*^rMJstA@BCc>K0~eoyro(4xFOo$Wy~3rpnZC)HOg>9W&bBw*tJC z3Ik^f$KDf{E)X>)q^E36ELUI#oTC7CEGl67{l^ zEU`3J`3K5Alrl${bEjuJf7Y2A69k3bf(!nbmW)f}gw-L-cmq01{?rr*Flnm5wo27; zrmj0dqPHK^HH67Z7^W*BE z6=|2T&s9t-vw;96?_vJzf^f9(R1^Od`fu2#m+!9w)IvI3N~-Ue$bO#&ABxI@YuR&B zr+q4euCB^xeZfzPo+WZ;|RO(_&z~7 zk7ZHX0yY!PJiRl*omfSQhjg0OK}`2V$`UJp6NC>@Gk37Dl!iPYAM4CCdw+1EeWp5$wiI_b;IOnY~}%Jl1;NJlZN;0NEs%&R%H--i+r4}ZOp(J?v`**axM5xqAfBCeP#e_cn#~jJ* z%)>PE!OPf)riu^w*zQEjS?v2+VYb(~ZzFtOa^4;^%QKgLr6gtX{%e-qZegEgvh902 z(}fG7;i5IgKo#;+I_;6a$SXBiqK=c~?>p6)pz_CI0X@yQH3L;%gi(FBKA)Jk9GXpM zUm9Q00{gz?<;O_MEb_7xVl+FrHCJEMjA!_22b^o7t{rpLFH_u@F_EYDnx#ieAPfMy zLNNjGV)_Qh4Jwj@OALq$G*+cT`{raqh7Z1n=1bvOr)GIkRzLFWj|(SH8@P$rH8=z@ z(cL>cy0PM|4ZiOO7eolB8o_EL|xb z=hy~k1lgXmrxvZ_8(#;%H8+t98hNHX7Riz2WuAmCu28tHAbQ>fvbf3@z#b|fBUQlem%u|jI6O6ZsT0lvq=^W=#pT&wS{-e?(o{Z^ zP`%+0a6|u&i!sEz=kTE-6%4_ZrNM4k4dwHcU(l#qHaF|DCJ3i$fNi6BEEj=uvx86F z$P0zg)1L`$h46;*$$4xiNNcg)QZ9XGGm_n06AxK#Y$tckmX|aI0&?xbY$Fd3Dl+ zzui8CpEZoA*^GY#y05E@K+zkv1t>0C?P+y2gN*sol3ZvxOQ4?zhbT+^n|w7vdoBt5 z_X5%sf)0ZVpKk!FK?8xITL(OR(?$yD%}ok)pVys(pF4D`Jk7Lg9{ptXjw}tnB;YzV zDo<8U67Qo1ob`|KJn6yg?TUQc1G5IXkS|Q|Lc|wZEHnCaQ{-b!wNMGV zjWtRlP7Ipq(J3IN?s8A2?=j7<$BxlK)9{M&?AGn38G3~mCpE5GQp~Pg2G#1f)vC}p&qalw)U}x1xou?u`vGIg;iDTcrJbA|3 z9ipr!wHLa}L!jv@K^*=mAtXKma;sa>{)+8Rd97o7b6;aQHR-I3+T6UIuU0k0SQq9% zia9;Ef9->}x%msril1aH$&)ainmErioF5f%R&qBBwFmIpS&Q8URf!egC1_CmX5CI6 zPYMKtcj8vlR(oKuxvm2hjn|Y@`FPu6;q5m4SZ{#u|Ez#0!s$ap{$rDC0Fkv$LA1uV z9QvPGALGwNt%ZKLfwzC2&v7cQ(R>0^><=aJg=NL>>hv*t7h^^bpPzV6x_7#kkzQaI z$Q*_t_CytxBC2hwAFHMwL*eLId2Jz!z{C_8Q?~0laEs+E+hjo;P&NJb=N&Z+*7?sq^Z$<* zO66<7{TL7Xmz%{TG3I}8&JoUUn1*uR8;S^-Mu42(erGaIS96D)-ZxtLZY&52&|)?GX!XjM z{@-$wDUv&XIhiarx@%2sP&^hAsPq77m3fmJkXe{DZ6Uk%gq^{4j>i^%h48v&~1pN(^+DMqF#d zXF=_eN9M4Nu@Knc)(U$YvlU;ErT2B>5VxH@q%2(Q$)j<+cMW0BKIYE=I z#Y|f6#nYd>IK4i9UY%P_VEy*D4;|A}0ZYsWrs=>vO2oN51+BcNx^RmFHqZ#&ls9*M ztT^5UC~zI+tgDG)rTy8B>r?XJ_Cq3tmA*|N0!|Px>u_{RkCuDB54DnRaL8 z_L|Sa5tv}2_iQAx*_A2b8ya>!eSyBm4O0qodMg8(-$NP_NeYtSsn`W`n>=Utq3_J$mx;9-GCy39P5aW8ri{z$?AbjcVx+to~~d>$6B& zXky+96X+*XRsy}VZJMY$pRFXW#OrRv*-EEGfP-n|nFql(yKa8AQ)pW8@~g;aSJo5@ z$6Ck8kOPQ(QF*y_ZpiEug`9Bi_77QR#y`7_E)g7%q+&9Kia@YSaGabsH=f^dgny4R zezb!m zNWXFgD0*htJ@5|{^J?WVO8u7wEa&-w>cgxZg}bO-7I`Rud0k30%j1bfY)68xQCC5- zD;o5cM(9J&Yo^~a0uy%Ecl@Q|h)H3RBEb+u09sbT+kG?O8gCt037WCQ!NH}1^F?W5 z2;vhi?~)+lj}r|KJ9aM+-X5@tb6;t z5l*Wqx;lKnQ1|&5UCY){em&C%)QGZjD_(=oooXWj7j3-QIu-q|GfN!)Mz|*(*}h$r z{N)w34WRbu->~2ho8-~%Wt-;DWI@h~unhlu&E>f;aiVuoR2HWr_bB1nnueC++VqoX zu3D>7n#M28sAi?u#INg-(&MRrVTPEYnvlySSzi5gO>IEPH24RHAv7O+t2nj=!O1rlX1|6(YOB4}bva!S(*F7@RX)irhl8j| zkiL2!&w#YeTs)+Is`sgJnFbzx*xUtjs2u2Nl zwqj*q;N){~_-^~RO-&}fy3D&5TO9k?Zs$!nh-=0s0mxQ!o=q9NAc|Isbed1jDPD~( z&Tw9f+_t8qwv!Yu@em*L)BQ8-7OQta#FvOGy)7-ac(4>gfkA*p-uLiuSnac*+BH4= z0dD@f8G~Z8=~m2Jwcagm0jGlxv}tkTtgdRPsS0a3rhIYd-(TM&`A>(Ds2s5u$ya(8 z|JaKXyQtwf!WfH}m*F7}8^=lQ1sEh(vZUNVFwqpiXH0IR2TMej7WH2-gs^Pli5@^N7uor~e2xeyO zA652n3<<>QJ$Tb$6TW~&Kp%+sU=2BZ^29SXBI(y2tXb$V3_2@TI_BxGf4~+;7w`xqbeh|Qcqi_;2Q3seZ} zzNE~HJesiC{RqCm9k(Zat)A)%1a5h2Qe;3_XK$2WG>rDcs=dt|w{DC8NZ$I$&xu4Y zl_*x_ZQyjB-~4Fvi;CbKJgu&taFq-#zWnYM_vR1CedsqFGZ<<>4{ruDAn1;8mS4hu z$+U(rero;P_fpn*;6&10L;O2s_>h|Xy!jE4zZIy62G!Um0Xbki#=w?B!z4W~nCYr_ z7tAO&v-!uN!4R4Z)2vgW8FNOFc?dscSnJ=}XIF&0H*nxSOhUE->OF3n6`NOVfZ|wr zNG<)3fbHl$6F=cTy*eW7`_pkT&imr455u7sB?WFmX*c$cA^$rD(I9~yOC~|K?!ldx zH6JpLj*OQ&nH%Q=dqBC-z_tna_}9z}2lD~f2OgO(w90N!_0Q^DQh@U6B+!uQ3R;MNdv;Pz^vjglhVkd|V z$tY$97qhmbopeDpGe-FMkH5Kv9jffm14ks;L=Q|@(-06qmuKSlIN&(iN4ZGv+j zBT)GF8U%uKUib|*-qlklr3=u}ndDeZJ1;pt%{Ck*TWE6yAR)JpvmpfUnIY!vA~NHO z4$GPR{?579C$~H^udg7hTb9FG#RX^TnzIpXFfHC$62j_fk1^9$F>M=L@V7M(4sU3` z@7L1VaiStJ zfZT&i{sQHD(uhL!4kVKAp1Q|TC%0%w5}wTi%*fmvz8^xGdAXvb?)h+*z#`MWjBY`M zy>{-cx4Mi}?=%EVj6C=D)qnG00{;?_jlA8QP6{8-pe>?)mD+sYFvD|b8UR&`nXrT z0Z?iDIKGoVfmmMY^Lhm{ZXdT)2c6E5AbpU(qCBkq(NR;hy!g@GmV(ke+FK8XOdz7W zNjQr;N=OU-BFTjq!9^tMW-r;OeC7N)82BG(w9sc!Q`T)K0%d}b3|H)-h#={WQIPX# zWf|r}L|S}#hi2s+o?(uprrtXcff&x#>C&K5^clbukndZIv+()!z*+T*}E<+vM4V&@Tv*cg8d~jXB^|fF+gGl zWAns5YgSE{ye-f~ccV9}CKI=h*5=!snl9^vO?{j9h`gFyDosS3y{s5iGVhUqu>8Vn zo+cHJ&^|zZRH4Cu`g{th?5c4p;-IusVyD2iag3}bn$?bWjSKw)W3PqKj1J2rN)*SaxgitSGfLK5xjc9h4O z3mJX*(KCN^V-cs1d1tFU?HOM zk|KS1IhyFT99yFyXP#V%TpKnX6S53dwmao01&r zF$v;p7;C1FxwW3UCOkg8H;YzS)gfMIWubczW@~0NRFp@am(n{BoC6~$+)bZG0SPKT zSyuTfYD1=*sv3A|IOgl#@g`nq?7yW8mt6jXQ+ez2=k|h1FsCM0n1HafHN2^eJxMCu z_!#AFYA!-^yTv& z&+GdRfEu%z=-4XC1v9_i0GA9^_2&81wjVfpywj|(LquecDSW~$ICb@`**W63wq`?l zM;p}}8?VOY2u{veb8Dz$BRk!~PRWO*-_h#T5o~Lh*3Ep*I$*%&0T(U+V<1Q)?mXn< z`z(~oEho@D7{{{>v`1;R_Q50?O6K*q<|67;MD;GH%b>eTqIW^HgmIlHyza!*XOBDvt6 zxm&M&`?a~iTd3#3cRHk=7-MJt@YPR$4yoo7YFv?pe$M3T4lLKTRPUM%ucouK#h*G& zydz7!WA;*0=lGjIE*Pk^@NxYKe7qB08_2$w%iQ15M`{H^A&VdLQ8hBLB}mi4q~hjl zpwNwbo<=%4W10^%FJ%IH(nh4`g^NX{u5>|Qy52$X(O|K4O>5Dt;|HojCvKdTx&gB4 zo?mE9v1Wk9HKWX=0CV}>o3AA!4COn+2L1$+NL{?$j3R~ zC<(-Eiy%9L{%h*fHo5fi`(n&~5#}wR7}1ipK-{HaskHsA)_DxlEedPA_3a>tTP|{1 zBB;zl^1+oBgIuka(^;Ogr|z^WLx8Q{yF4e41>49wkoN-V?Msn5nFS{WI+s$w72*W6 z^BX>7lQ{49UT&1nUFuV@!)XQ`OyDX`c_Dh+vZh@hzi$zMvO_e({6n|MuD_b3n=M15 zF&S%61chy*x_08dIYS3im8PhZ)>it~iBdhoupP=Ssg z!-$2{_NS4MYni0YBK))9MzdubC*;f!+O_Q_pV2V3n2rtzJ5BFv4H54BuYpfn!=**l z1tc75Xkmt5-k|{8!655s{BBWQRIX6b4*Rez?Hj|xUTNUrn@ipy9Ca9 z4r1(NfkJrMqc&%yLV@?rmW17%JyresIn+SH!KJiBP zV9aLZm)SCn>_V)bE~8D!@am&tfxyZ%e?+hOqQE!O}E_~r!f^u@9+62pI{O4V2`%tY$6d7ek5BQSJ~ zV?VHoIL;4EX_# z?4<11U6YU}d#ERF|J<0{!rSP(LenH6M6EeHp|;5)t{wNP&T5K*XYVx|6m-fJJCHL2 zn5>TBp9-cxarO(3Z45fn5_Q$OfeiHfRD>e+XGP!47%h4N$N%g%KgVqSIj%E4FOEMg zO_4_MZ#0?w&>U~fxSc`lrv*TP9}u6iFEYh{3{7t)P2Z6;$yI21Ss)zql#CT*8tO9} z0@Mn+Nlsnb1n?z&tC3j@=GW@U5>Vo zA0{O&`wDv&M9?MF9X#k5vjpf}_!g(djDEn1x z*Z#0#RcQ%EQfYzrPT_BCHa)Xu3Z8Gw*5&ew0M1rnHRG``N8#fz+|-G7qQP7Uy+XKu zURcdz(a?~7&c)P{JQ4h3wxZ3*Q*^&3_=F6Yf|R=dlXW#`cB1A=7Cip&33D}IoltCm z{S-XXpWX7#3Ax#Z)83Mn04@;umxG9G^|PY6sqIcr?**XO74lZCo3#w!pXF#t#eie) zsn7Mk7wb`(lxG#12#$ym7L)@sdvjCJ=*P!#BjFW%{R2-iHznPOg(F4PlS&MRLzYn- zWbm_LX8QD+kiq`2jG4j9@Z0=!|1-ysBE}ZI+=cD32adBd#fZi>9EXu2p_olyKP+Xd zSm!)D&e0-9^Ifk#ODocOmg&^4!SeMe9&c9&Xp?7p@zL=c>^wyU1dyx{n2@LfUCqvo zJ|fHnekuK7_PvnpW>w@MFU)aHekgtQHi%yFQk&d$9! z`x!+Cia@i&T3SzWCScCbn=Qg2j$FXyt3Ul|AG!yq4H}3|aVFftWMYVZPcVDxqJY)W zoX{4<^HmlT`j^63`U*t=AV}1;X!hCY9>eG?Nk@dYWjNC&%c(ph#*aC;3gTvG@1U;b zL&j=JC-A*LhU^G?veph2iFj?FdcS`sS6u%IpOAn|U+s~`hHJ8}ZkMFOZfl^>F;p?s zq+Y&W$i_M%e_N2-ueW%#?9bs25|c*qhHF);%7sT(@72*k90%WTNUO%r9Veo?>-pZ% zJGTPeBeyQ16Jxj!+%CN2d^Qdv%0d-Htr_}HheewXCCPoKt;8IOL=^OqW0IAn!83!@ z0b^TU{riKT0w(2!f$@D)@S;u>|RnKkP|_2J{_Z?!B&z5+#d(|M*AygDeQV2AVVPeNt-gkMb!X1vcuI zKQYkSnzlCQVwjbuH7#Y^rT?5uv9e^Dgm+4gY)9%pPv*ZV8j-W8;>Wzs(VQNCG626d z(36|lW7$izbhM;5n_vf153_KRyBi!!pwX7Fiv*|VOrx+>nSh8W&Xie+O<~dDN#%Yk z+ED=9CKl%=QscR%R=g7*@SB!ectSyO0e`7(cjIJzx{#XY_O05fHz7GTIJQ=3b7}ex zp|v%oyb#?9IvwP9owk+VD{#mREP?`bX;=9-LQ>@s%t$tQ2cdhE1Uc>ZRD6chNwaL{HTke6|# zWTCoRd!YC71vF5i(Gv$;fP;o1HtJ1Vz@I}DA>~PbMgD=xK0uu~j~m~y1r2is{DaVJXYODGT{&Gxt(od1*W zCAOjP@l|nk`ybrM<8bfHmakCke2c?=#HbB@MdNPXl-kxUBtiL$X?TzRPKBLdqNt`m zzMJP(;cu=+HWP0*rd6IoSFI&o654`;I%kD-ld+)42^mp$_I(ZDQssmuC}amJM5x*d zA(JSZ`mX2puE-eo%MDkdJ9D^Sr2~JweaM)?LLCUjDU!SuFaPeoq#30Unu*8xbv_CJ zkm~d(S*SnIRVaJn*q}LapVP$qKg6A7P+MK!uW4zaEyXDsq-gQtZpGc*2~HrmJ1y?+ z?(Xhx!QI{6y(jnmJny`p&c`$7OC~ePEXm$U*52#-U%&Cxy58ed!r(EB#w1)ZH|aDm z#CXVTZPS^t`9(vzZFpof&)-Ds2U$`%CyP)2cAIq>5`{1ce7bryp}=~DZGbYMYS^h& zCNoR`e|Suayy*(<=iwlyuK;0kinyNvo$;9#=S4=_j}Ok~erY~QyQV5P=&Tem3QX^< z-rcGSNLD$N0(T6pS+^F`zMmlLX`0nbpV5(Bs5uI>1me38YxTy~G^=nF9PN+vMHCcLD&WN6zp(2D{tAs6!#K)DvuKA^;XC(G!7&yR3X`k}2j*5`l-_qKUe0O}h@6e>t<{eSM@0|c^4tnU{ z`N|_<#?#(p;^U1L8RWRgN!I!#!`_S9o)N0o#43d+1K1@$Pi_5_e$7+qu&}N-ll{GG zxzE2l0-NSI!201d^rEaGRzkIo^l)9n1NItvFSE2f{h#R-n1=u z3HK=;LLy{S6+Z^+LBOM|?Kv|)BOz$)9M;T63A>yrL<2W8FLs@-;Ru@*+L+-OV!=u* zy%OdC6m&ksAtiLpwW3g08j<8aJ-BA_zSwGorjN?ZjV)3ko(Q-Icu7{rRz*`$U-z@n zR)Qhm($1gA>~6cgXJ9l!W>aOPQtTWrcq@>=zf*rDREc9|n^&g!w^&Iy64l5s#EEuLv=yd+eiMgkwl7mtf%B z3`^7&djGpNpGw}{^f`lw-eNT&mqc7Rm6$B$$TLbxLYk_uowWG6^0}4a@9hPcea1x~ zk@CEa6xh^Al_sjZXw@O5J&uMwI@MTZ=}@dQKVq2Frm&S9arqBBu%99_kyr)DOVCZj zqJ*WUi))yaR}KxKq%cQeA<(j#M4Fts?&EHVdZ8^s1q?C% z=?om48^ouJ-aXsjYIJR4i-sY3>X3VHX|!E)Z!ol zLoy`*8?QreN;74}R$ISRlgxtW8p&2BHYs3|wFDiTbzGLlgyv66+C{4XEC*5GTi%%F|1 z>t87~-E;Z)xQr5dEtFyWs2(;im%R!bGZK{ry1JlESWk0$E(VNP1Y8`OUvjf zhcB~CKRB0-r!!*rSF&_k(@m5fW_&ske+NvrJy5p3(d5orZIL9sOwBf3c;&OF1G~<@ zmVA6__|*()2jT}!!*t$%aeU9DcA=FMYRPI(N^qrQ!#CSdy4jJZs|XqNT4d*HJWwh! zu$N;@U(|84MbB$SzMORn7{G`a1J>X=@9nh}_2Q7Mtge}!cMSw*xqf#z#o!I%OfSJ% zp~GGmZgx`|g!~9>{n>wSIw2DxxX_P~7zCC({*9{_G%c~P+}A&11oFAsjX2rH(a7yS zC^{shI|&$CFm7$iNT@UY{p+~t#9D6)+y`G<3inwmJ#Ypomj?IRWb}+TL}j5MXuAF~ za$RJ811-mKAn=0~Su!4h`@}DGkpb>S?>GutZZGqB-7~-9%j^ElxKdgZemVVxkdT^= zOwNzh`7We};4w>3J^qM_spoOqu{}8@vAcdag(#xefD*O<>F)%NeG&)0>^L5!Y)q+r z?QG})2h9Y?}_fX<%BugA+h zlLw>$efxYyi;=cY%H@||OtizX92#$QswNTzYR+*UXpY%v5G^x+PHw}W*5%(K-QLIE z^_pVCt}v!9g?C)ZgONpxgu2z#O%Ad72+sv@!hl+U#T1*FZzL$*pU)O?;yO{wV)dK2 za~~)b&kWbtj_DwxNT0Y1d>|kddlP2Ac|a6i6S`uB#ec68^aQWnP zmf-D)rCabbzqvU1*SUdu? zC?R2eQ!6QOK3Y8vdf@O7*m~HdftEMyiAGp5siOKRf~mRVyR3SZm}c3Xdq)Ue)Uuzz zk5drkiv1?saQb|{VYqtWQpi`|?Yv$~@bfsuLQ|Pfz+KJ=q80huDmU7Q(&tS*+kTz| zvpee5*y3xOU1~FMVZ7Rglk*t=Lx1s0GBhdgJ_bK;33egfq6eWBP9^FE{6Lm(P#<`l zXF;%5QiRG{-o}z5TQ#!H1!=g-VU}Z0JSuLWvED^z9_9O!H~c)(PLI=T%IXVN~2R^24|7Cqp)SkE`u?FG65Mgb+nr0dgG$owV{kg=gprwdb-fNCLH#2Ci6o4v{DLNrk@Gb|593`ELATYc6?X&UMWb^`*$f)8+GSC#`9S9SYQP;r(%F11oqL zKL1_IeV9zxt14>)jd=|w$B300D63}~8#xu8uYjl&6;o2fl5=EnAiQx~0t!Jn(tcyE znqeKsFP8!QS_?L?KL}Bw#g$AcD)8V=B0S+yiJP3AuFpp6IkYi5Q03f;O?#Y3qB zb}(~?lOP*eHgiM0XzWm~s2WJ{7e=7m$-DXXvMoIYu#RfTtoI-HSbN0I{7~+)tjvk* zx3n2{uFo|$NbxI{9x(V4C9LN96+HW5ngNK{n$c0E?bab@<=w5(^6Xu-R*xqWty1?Z z54GyRy>u}zD9dN1J8AeACQ#Io%PqM>pXa`!?V?xL?pl_VZyy>HLw}HNwiEF37v^pk zYG3?L_~>{~SefxM{Jj2E_@0mXO7P9=+4e2yf7{~QLj4Pq0JeGl{1;~Q|5IDC%vS>7 zhPRcD>@TvU0O&ME6K)j$@r8{vG>xAWyd0mq(_*c^`XFCEHOLO-cJ$tQ|r7s7_+f1gsm-gy0K8+?VzlF~fR z{}4Y{=3eoPdHuJw_5U=6@pi}8y|#T9^X72hjuvvZ6*GAM25moy{=he8@bot0FASsZ zB%d4MCbUD#w}bn3l#p`*vmHC=C;n#Gk$n&04);1Ie2jhz_Hu!~$u0xhc{9(sAMx{e zvJ2oZ%o-nb$S4Xfc>jHh@pcC7aL1AGEW&MLu>E!~UF3hpP7j^%`hPnwxYs?&u;n7W z7u*PJzoX<1`%fn!>&#ayP>H`6bgGoEx0W~ zV%59cd5_eN<8mGh-b$&0a70*mz6V_!R?M{L+NyzN1>!*w8WezD^G9=H>e;}5fsKlK zRYRM$q9T-yTC_2G6A2Nu#jWj3U8yBqQsxwjoolqdONjwxgULFdu#g%I!$#P(ZHt== zE$L(Q%!guPy1wvnekz+{_7yrdpI!%8-RP(2$NB*jR{!KzLhw=CP2BF~^lq;!LC4|c zTk3eOkZ@4zzZ9<)-FhdEt}HvD8IIW{{B!qN0^#o0&|28E^}jG*rn_a5^bQCY=qHA)OENzMCKzcWyQr@GM^l42x*x~5m}!UAn1 zM#)7zv^Ci#VeglFkujmirgeSR+Od&*a<1idE#c6mh5MyuYS|;ho(M$%3qDZGcvAm) zxyvF54ETM$CP2#iRzsqlT)6b(YC7og7hAI(jho?1;<@W%DOU2b$>d(CMoT-J_U)N{ z&+kc=?S2qP=Zl)hNbu75vj)?B;UWHcL1;y;>XcR0Cra-cp^0uU-+X7j)wuNAYs-SU z^02=!p8hGD=$g=pKBXf~vMu+Bj$S%UwLBaZ2nipw-nOuYpaw@%nHqsWVNC)S!h@Go z?fK=|xog=jWPD-0WuLqQa~0)hW-@kepvtvUvJdvEwac_Y5YS{b>YQlAptibjHR?ac zudcHFSQwuD8-c-plPc497tUCX15U}lArl66TumZDRa4%fT*&{$txj1H180^UL}??~ zzUHhcm$44aivzhUyE(NQUzi_C%^h1%qPH-6J{{rNUA}&kmZ8>F^tja;gO79wXf*Z9 zdrj#syzON`h=OXZyo~d>?~MyRyHjO09}qg$ph1(z`OAQ?5mBLjX6v!@$X8`;nP~&N zmCwD>+&TZGEykf(rRTf)x$H$NKCDPgLGigC3y=SaTAZ{kHM^jZ$zJ@i3tQ4J3;Ae3 zmz)7S)&(+8^m4w}@*1dKx2oD*Cq4M884Ko)r{(piv>E@bM*{nNHuPx%*`m-5UROV96Qy;A<)SC%s(4w`xe^yNdhSg_XSq=7@_ z!#rL5b|FD@q%orv&;^YN035ey08Bp%!xHeNrgBF>&-e$RdiXbH@>km(WBaE7w;(M7EQ$k}&E836h9}V%?f~C6?D;?atA8b^9f-DuwI*R=HRzD8x zWXjeqCZ9&wmJ>6q9xP#ag<^ckq}9H{Thu!az^2PNw)cqa^EbXhc)_&%mQQXLk=16}f> z1!vF5EdD2NGu1|%x(pp~J(+#*Q`iU&K4|dNNC~tPBqPpS(>I3cZ@_=sq#BZj^`P2id#f(V+@!e<(|K9O2PC!=B(^wxb$B$?W=( z-KkAeH7Cj6iAQ#VV*=a#Vy~hK7`v{ePL0A7xq7skmKVf|mkkyaWZLUAnwcxQy8y0{e1Q6U4uS`1pBF=Tfjb#4bkX4_m=T?>bN z9TTJLlIrFOPV#;~g7ykRf>!KhhSOEJ+k-KRKy4^uArOEWDTtCvmPNtybvJGWCt8s?K)P*_g_@(7IK4`qL4yW%3l0 z=>|N)A5T^T6Bm`>YKg$5{{9KvccgbbA-@MM&68aylgOvU)qf;kfAU7{e>VZ!|Ds&l zA}GlgJ6Mty#47PMOfPa!5+0}&?^G-x+4btSb7x zo7#G5eyfobqeh|M#9wWbrqL=dzZ15A?D{mWzlf6h@`STEYf^DbhO7>{^b^|H(r{MUNa5G3vN1uWC&m_kF5?BtD>H3A}+Bz`PT zaO|$fC9o)sjI^fppTR6AY2Y(dfq*1#jj@8crAtymqx|u#Smt-2H9qCMB}HZjhY1U})O#$RzA^5h=02=U^iFN~5c{P8^p@)9 zj=feC?vO}a&KBWD*6&gq`w5thcx)YSV#HyH1K$`~n*~}e#LZ3y;rHrTUa|vqi?9?6 zO)ZM?m%>$l38xvy6xe=hH^#kgi-YdXZfj#MX(VnJ|Nhmj0iaF1RLL`ouo--CF?+-YN14%#veY=bHC*cy@G7-)b3+EMB-&Uuj zYizVrqR9HfjT_fHYU{qbA!ZfQ7Gzu4`U+TREI*g~MXk=wKvh0$1&O|1IiWAH{h&8$ z9xkuwR`mg>H}aZl%(6Oi-A-bBi}Wo-us#JNIqf-f@MQC#lAz7XPm7BXO3~Lwe;PQ4 za zu(qAg_fnjprLeN*)A$|^idyE>JKufJtS%ON@&Z_7-X(vx)R_hw^o#D)kZ>}uy-Iur zGmJ3ARkw`;G{+Ym%xZ2?CV`1sVHVY#a&)pVVqbju309Apc`E@C$}G|6W(LTl65Uz@ z;lv}w%kh)o4;rnriJF$w-1GQlp(33he)|faeY1C9WSB>)+=ENdMw;ukFv#rF$4Qb$ zK_>CG*4k5Hn&yb$uQLv#=VaVUGsEo=@rthJ0@!Pc`-q*bgkCqMY9Bg+`0?kOH|Mdc z+$5E28c*rIi`gb>F9D&d-zRAv0fdtU{~lRZ1UQZwMGs29u`Uv=uG^^aaM;AX)-@_< z5gTvD4FLrKuepwW(zI+yK+lDkF3Db@b7TSdWiq;c5@#>F0_vj|UGbNd5nF29^IHu%1O`J&8W z$-1m|kb5;BIDJYH`=XHTU(O|TDFx$EV~At*d17s3|5i?hjMsK0f_!s|P4HALSW!`) zI(g_rE|%$MBr;HLHZ)f2r+~u3c(H~pq5h<+O|9u8Qb|}EDE4C8L4oNiqCxN)%5z1dkeN8MDuAhzM@{;O!RzDI>w zb4^^XAF;-MpiXBIz`<+$CSYu8*V}HKknHlAAYQTXT+B(@rcEmp{?`Cr++=X*`v|pb z-j$S8zbmz&S~YIkap{1B+g9o6B?qCV)9K$^KSI*CJKJ&(XJyNCdW%WIBBz@sy=lHW z;+l<82i~pC3nieapI6q2~(+7=CUFzTvZiad$=)6*Btnf??Xj7+pmZE7O8%jMDmQo#7F8}xadlAlu}+=W`wI){kXq#=vrltI%h+OF>=!e zl-o<2PWq82JG)`Uj`^x5J4sQ5ElA2Xp&XMhnHvxsebE-powAKu*ZZc`qN#Kom?Sm6 zhD33#bP^RBz0sP{{OoB#6?jMho69KbrAd3@Hc1It0x0N{XsRO6t-@-oCr=9pZbd7z z?A5CpvDFyXy$)m8lU1{d85x$shbZ|-nUX}0=_l{Yc@PBpA6+U?5diIW5&`8O7fac- z`IFQ3sr7+AW-S4VGc;)!mHtX_sppmfI}%}}V57W1c7!ANVmZq~!||14=f_n&s0LP(dL~S`*o-9=66mx${dJQ26Y|{Ya+4{2YFDO7e@|LzDdIRY3qs2 z=tB3`wvD!n$uZLeIG=yTe3Ysvhzmyy??ff36%QOZuGpY61rxRM+nQM;WR-NYy3`CY-_|jn zyYVx_tQ#cw$m@XzeinVVRNO`HSKB(JPt-WBSRNIrVK}z~2uU5H^6r4C+;FWq%|BaT zlEyOB_HQ}R+OW;qy3z74ok-xi`f4JqeSCgSH~{FFQroCsP%W4f;!7EftwztKm;ndN zi4nb{{Y;CJ%}LjUnRLEFU>a?!CRMfUc$n`?Q={;;RF*bomU(Kg^k+EIaDd`&x5QgV zRgsC`=@V067XT}+Zx%;u@~r5%eRpxU*lv_Pq!zgQ9IsjWR$nZwAo|NplCmF#ztMV_ zg@Se2=`7s58-n*N7+RY{giet|a9gV@yY%L@t5N?!p!@(vbyb14Ek5SYGL)5xMH@uLo^)(dX4^N2=&J7bR($ySlsj|{Grg)9#dJ>evr zj~;%*jtlxX?5(vlUD!|A{oKl0%syNB9&CPcpm4%4l}R?HQrm{9$aJC1hgCW}8Yc^= zZ&N#q?!&uH2WwVYjO2JWOyRFUQL(EDu`T%EW9w20%c>>fBiv8cE1Wz87n62YtUI{| zZchF^nq(i;EU12Dk@=fwoLO%h0%no=4X{)E2$@kP``1gO&PiEUPA$bXZ2;Iqzg`%9 z3l-&R1Godv9tWq+*l_IuaBULTVAI`zFM0jeoJUpu$`;LQ870oUkI!)s9@fv8-5U>M z^q->@r2C!~*_%4w%laY-r!e_X*M7bv6ue|K~>%&j)hocHSZ_fBgQOK`!cm;Iyw@M$! z_sk?&0@Ir<`cB+}{2%m;TXHKK@92@{XNTnDeX(UObQ|m}Zf0#G*BnCw9IsB{i(wR~ zNE~R4YPV~rJ@#$pg)o&hb%O+xlCsua`>Mitj+NNQ$hc+Rl(_)HBgumN9UnF3gn8uG zzV6(OrUpbPw`cYLsNw9(-Gi~Ngj#w?F$GZ!5v|_J&35!gxAxOy4~qA`p({z6eK?8H zgmtr@RG#6EzYANtPFp7n31Sc>BE`G|KN4eNpkfX&*~YKH>=B}58PG7yjZO~@JJ4!H zGdK11F2JKWZ1+i)nD(X)sm*ch7}Pxa9S{NirhOd`nyT^3CK@P4cvdiMIv>RFp z@Qt4rXQ5ukF}g+?KxS>{+pVz(!96{cX0D3BSY+GX8dCZRs?bJ#JQQh$U8xtHD9L6OI6rnv#kls5I8gtr3?!-Z03ez7e-{o}{c z#8vw(a-KQMp$66n0!r@Zrk!Z<&rK%+-&p+HS0J zn)>!&MoPzE&_h zx{pBN)~Z}V9Zl!kZi4%2SekEEcik9?*9lWBMi=9sTkhMOR3l4Ln6c!~os}?iAK{`b zFHUOZmT$@d4sJ&j{}dL4f*UsfUQY{Lm+ESfY3(YuAb?g)1>}K9LV$Jh&d~@ZG!o)%8 zRm#zQkF_012fU?4<3dbJNliCf!nJIUaXaZCqyyJ$1Vt`vw{f=SA#qk=U!(Y7Ojw?A|MIfsMp6pvXgUD&qre6>^fMW$CMd5PE*cy z^4s&jFo4M)HS*_9mrwqgg){qGi7}a~D>g~J*a~AK5>a^jMJ<50{1<6(cxESyg(l8c>BS2I zYOh(Y;Z1eFU`4BSeH+V#P%&j8p#qLA9s->%1<`DK1u9ZbbhQ6vXE#8W?P|VBs{^*T zkK{m8io_e`9+t#P7U95c#?>Rd7xp$fm7z0C%S%Ww8I9#hisiW{I4y&EGwHP+pQ$16 zs2r!tZvB{Q3w&0aF)0;bMC?!T*#=Kjm!IwW#}BI1gUy!C+*t>L`$a#G}Vb`ivbPOLJEdgoOP)J358y$PAJP*Uzg#1xYmez$?2N z1XPqJGVDFa`#yS^BC_7$`vn&<=Fct2QrRR$0gdTwyJ^J3<)G9t!T3UgGrk9o=vw5U z6PV6Vd;X#Dq@ue%hOp-z+k!F&&0id$)NXYx>L}Aiq`L>|`m@49P{#|wUP$I5KK<}! z`ZET$vuMX&K{a6yda)?o7r@`#itu5PofR3o%v~y3P4}*Vf%eA!gY%0Dd(z@*0`r4V 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