mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 22:11:02 +00:00
Adds Snowbridge to Rococo runtime (#2522)
# Description Adds Snowbridge to the Rococo bridge hub runtime. Includes config changes required in Rococo asset hub. --------- Co-authored-by: Alistair Singh <alistair.singh7@gmail.com> Co-authored-by: ron <yrong1997@gmail.com> Co-authored-by: Vincent Geddes <vincent.geddes@hey.com> Co-authored-by: claravanstaden <Cats 4 life!>
This commit is contained in:
Generated
+888
-66
File diff suppressed because it is too large
Load Diff
+14
@@ -36,6 +36,20 @@ members = [
|
||||
"bridges/primitives/test-utils",
|
||||
"bridges/primitives/xcm-bridge-hub",
|
||||
"bridges/primitives/xcm-bridge-hub-router",
|
||||
"bridges/snowbridge/parachain/pallets/ethereum-beacon-client",
|
||||
"bridges/snowbridge/parachain/pallets/inbound-queue",
|
||||
"bridges/snowbridge/parachain/pallets/outbound-queue",
|
||||
"bridges/snowbridge/parachain/pallets/outbound-queue/merkle-tree",
|
||||
"bridges/snowbridge/parachain/pallets/outbound-queue/runtime-api",
|
||||
"bridges/snowbridge/parachain/pallets/system",
|
||||
"bridges/snowbridge/parachain/pallets/system/runtime-api",
|
||||
"bridges/snowbridge/parachain/primitives/beacon",
|
||||
"bridges/snowbridge/parachain/primitives/core",
|
||||
"bridges/snowbridge/parachain/primitives/ethereum",
|
||||
"bridges/snowbridge/parachain/primitives/router",
|
||||
"bridges/snowbridge/parachain/runtime/rococo-common",
|
||||
"bridges/snowbridge/parachain/runtime/runtime-common",
|
||||
"bridges/snowbridge/parachain/runtime/tests",
|
||||
"cumulus/client/cli",
|
||||
"cumulus/client/collator",
|
||||
"cumulus/client/consensus/aura",
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,127 @@
|
||||
# Snowbridge ·
|
||||
[]
|
||||
(https://codecov.io/gh/Snowfork/snowbridge)
|
||||

|
||||
|
||||
Snowbridge is a trustless bridge between Polkadot and Ethereum. For documentation, visit https://docs.snowbridge.network.
|
||||
|
||||
## Components
|
||||
|
||||
### Parachain
|
||||
|
||||
Polkadot parachain and our pallets. See [parachain/README.md](https://github.com/Snowfork/snowbridge/blob/main/parachain/README.md).
|
||||
|
||||
### Contracts
|
||||
|
||||
Ethereum contracts and unit tests. See [contracts/README.md](https://github.com/Snowfork/snowbridge/blob/main/contracts/README.md)
|
||||
|
||||
### Relayer
|
||||
|
||||
Off-chain relayer services for relaying messages between Polkadot and Ethereum. See
|
||||
[relayer/README.md](https://github.com/Snowfork/snowbridge/blob/main/relayer/README.md)
|
||||
|
||||
### Local Testnet
|
||||
|
||||
Scripts to provision a local testnet, running the above services to bridge between local deployments of Polkadot and
|
||||
Ethereum. See [web/packages/test/README.md](https://github.com/Snowfork/snowbridge/blob/main/web/packages/test/README.md).
|
||||
|
||||
### Smoke Tests
|
||||
|
||||
Integration tests for our local testnet. See [smoketest/README.md](https://github.com/Snowfork/snowbridge/blob/main/smoketest/README.md).
|
||||
|
||||
## Development
|
||||
|
||||
We use the Nix package manager to provide a reproducible and maintainable developer environment.
|
||||
|
||||
After [installing nix](https://nixos.org/download.html) Nix, enable [flakes](https://nixos.wiki/wiki/Flakes):
|
||||
|
||||
```sh
|
||||
mkdir -p ~/.config/nix
|
||||
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
|
||||
```
|
||||
|
||||
Then activate a developer shell in the root of our repo, where
|
||||
[`flake.nix`](https://github.com/Snowfork/snowbridge/blob/main/flake.nix) is located:
|
||||
|
||||
```sh
|
||||
nix develop
|
||||
```
|
||||
|
||||
Also make sure to run this initialization script once:
|
||||
```sh
|
||||
scripts/init.sh
|
||||
```
|
||||
|
||||
### Support for code editors
|
||||
|
||||
To ensure your code editor (such as VS Code) can execute tools in the nix shell, startup your editor within the
|
||||
interactive shell.
|
||||
|
||||
Example for VS Code:
|
||||
|
||||
```sh
|
||||
nix develop
|
||||
code .
|
||||
```
|
||||
|
||||
### Custom shells
|
||||
|
||||
The developer shell is bash by default. To preserve your existing shell:
|
||||
|
||||
```sh
|
||||
nix develop --command $SHELL
|
||||
```
|
||||
|
||||
### Automatic developer shells
|
||||
|
||||
To automatically enter the developer shell whenever you open the project, install
|
||||
[`direnv`](https://direnv.net/docs/installation.html) and use the template `.envrc`:
|
||||
|
||||
```sh
|
||||
cp .envrc.example .envrc
|
||||
direnv allow
|
||||
```
|
||||
|
||||
### Upgrading the Rust toolchain
|
||||
|
||||
Sometimes we would like to upgrade rust toolchain. First update `parachain/rust-toolchain.toml` as required and then
|
||||
update `flake.lock` running
|
||||
```sh
|
||||
nix flake lock --update-input rust-overlay
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Check the contents of all `.envrc` files.
|
||||
|
||||
Remove untracked files:
|
||||
```sh
|
||||
git clean -idx
|
||||
```
|
||||
|
||||
Ensure that the current Rust toolchain is the one selected in `scripts/init.sh`.
|
||||
|
||||
Ensure submodules are up-to-date:
|
||||
```sh
|
||||
git submodule update
|
||||
```
|
||||
|
||||
Check untracked files & directories:
|
||||
```sh
|
||||
git clean -ndx | awk '{print $3}'
|
||||
```
|
||||
After removing `node_modules` directories (eg. with `git clean above`), clear the pnpm cache:
|
||||
```sh
|
||||
pnpm store prune
|
||||
```
|
||||
|
||||
Check Nix config in `~/.config/nix/nix.conf`.
|
||||
|
||||
Run a pure developer shell (note that this removes access to your local tools):
|
||||
```sh
|
||||
nix develop -i --pure-eval
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
The security policy and procedures can be found in SECURITY.md.
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,155 @@
|
||||
# Parachain modules
|
||||
|
||||
## Configuration
|
||||
|
||||
Note: This section is not necessary for local development, as there are scripts to auto-configure the parachain in the
|
||||
[test directory](https://github.com/Snowfork/snowbridge/blob/main/web/packages/test).
|
||||
|
||||
For a fully operational chain, further configuration of the initial chain spec is required. The specific configuration will
|
||||
depend heavily on your environment, so this guide will remain high-level.
|
||||
|
||||
After completing a release build of the parachain, build an initial spec for the snowbase runtime:
|
||||
|
||||
```bash
|
||||
target/release/snowbridge build-spec --chain snowbase --disable-default-bootnode > spec.json
|
||||
```
|
||||
|
||||
Now edit the spec and configure the following:
|
||||
1. Recently finalized ethereum header and difficulty for the ethereum light client
|
||||
2. Contract addresses for the Ether, Erc20, and Dot apps.
|
||||
3. Authorized principal for the basic channel
|
||||
|
||||
For an example configuration, consult the [setup script](https://github.com/Snowfork/snowbridge/blob/main/web/packages/test/scripts/start-services.sh)
|
||||
for our local development stack. Specifically the `start_polkadot_launch` bash function.
|
||||
|
||||
## Tests
|
||||
|
||||
To run the parachain tests locally, use `cargo test --workspace`. For the full suite of tests, use
|
||||
`cargo test --workspace --features runtime-benchmarks`.
|
||||
|
||||
Optionally exclude the top-level and runtime crates:
|
||||
|
||||
```bash
|
||||
cargo test --workspace \
|
||||
--features runtime-benchmarks \
|
||||
--exclude snowbridge \
|
||||
--exclude snowbridge-runtime \
|
||||
--exclude snowblink-runtime \
|
||||
--exclude snowbase-runtime
|
||||
```
|
||||
|
||||
### Updating test data for inbound channel unit tests
|
||||
|
||||
To regenerate the test data, use a test with multiple `submit` calls in `ethereum/test/test_basic_outbound_channel.js`, eg.
|
||||
"should increment nonces correctly".
|
||||
|
||||
Add the following preamble:
|
||||
|
||||
```javascript
|
||||
const rlp = require("rlp");
|
||||
const contract = BasicOutboundChannel;
|
||||
const signature = 'Message(address,address,uint64,uint64,bytes)';
|
||||
```
|
||||
|
||||
For each encoded log you want to create, find a transaction object `tx` returned from a `submit` call and run this:
|
||||
|
||||
```javascript
|
||||
const rawLog = tx.receipt.rawLogs[0];
|
||||
const encodedLog = rlp.encode([rawLog.address, rawLog.topics, rawLog.data]).toString("hex");
|
||||
console.log(`encodedLog: ${encodedLog}`);
|
||||
const iface = new ethers.utils.Interface(contract.abi);
|
||||
const decodedEventLog = iface.decodeEventLog(
|
||||
signature,
|
||||
rawLog.data,
|
||||
rawLog.topics,
|
||||
);
|
||||
console.log(`decoded rawLog.data: ${JSON.stringify(decodedEventLog)}`);
|
||||
```
|
||||
|
||||
Place the `encodedLog` string in the `message.data` field in the test data. Use the `decoded rawLog.data` field to
|
||||
update the comments with the decoded log data.
|
||||
|
||||
## Generating pallet weights from benchmarks
|
||||
|
||||
Build the parachain with the runtime benchmark flags for the chosen runtime:
|
||||
|
||||
```bash
|
||||
runtime=snowbase
|
||||
cargo build \
|
||||
--release \
|
||||
--no-default-features \
|
||||
--features "$runtime-native,rococo-native,runtime-benchmarks,$runtime-runtime-benchmarks" \
|
||||
--bin snowbridge
|
||||
```
|
||||
|
||||
List available pallets and their benchmarks:
|
||||
|
||||
```bash
|
||||
./target/release/snowbridge benchmark pallet --chain $runtime --list
|
||||
```
|
||||
|
||||
Run a benchmark for a pallet, generating weights:
|
||||
|
||||
```bash
|
||||
target/release/snowbridge benchmark pallet \
|
||||
--chain=$runtime \
|
||||
--execution=wasm \
|
||||
--wasm-execution=compiled \
|
||||
--pallet=basic_channel_inbound \
|
||||
--extra \
|
||||
--extrinsic=* \
|
||||
--repeat=20 \
|
||||
--steps=50 \
|
||||
--output=pallets/basic-channel/src/inbound/weights.rs \
|
||||
--template=templates/module-weight-template.hbs
|
||||
```
|
||||
|
||||
## Generating beacon test fixtures and benchmarking data
|
||||
|
||||
### Minimal Spec
|
||||
|
||||
To generate `minimal` test data and benchmarking data, make sure to start the local E2E setup to spin up a local beacon
|
||||
node instance to connect to:
|
||||
|
||||
```bash
|
||||
cd web/packages/test
|
||||
./scripts/start-services.sh
|
||||
```
|
||||
|
||||
Wait for output `Testnet has been initialized`.
|
||||
|
||||
In a separate terminal, from the `snowbridge` directory, run:
|
||||
|
||||
```bash
|
||||
mage -d relayer build && relayer/build/snowbridge-relay generate-beacon-data --spec "minimal" && cd parachain &&
|
||||
cargo +nightly fmt -- --config-path rustfmt.toml && cd -
|
||||
```
|
||||
|
||||
### Mainnet Spec
|
||||
|
||||
We only use the mainnet spec for generating fixtures for pallet weight benchmarks.
|
||||
|
||||
To generate the data we can connect to the Lodestar Goerli public node. The script already connects to the Lodestar node,
|
||||
so no need to start up additional services. In the event of the Lodestar node not being available, you can start up your
|
||||
own stack with these commands:
|
||||
|
||||
```bash
|
||||
cd web/packages/test
|
||||
./scripts/start-goerli.sh
|
||||
```
|
||||
|
||||
From the `snowbridge` directory, run:
|
||||
|
||||
```bash
|
||||
mage -d relayer build && relayer/build/snowbridge-relay generate-beacon-data --spec "mainnet" && cd parachain &&
|
||||
cargo +nightly fmt -- --config-path rustfmt.toml && cd -
|
||||
```
|
||||
|
||||
### Benchmarking tests
|
||||
|
||||
To run the benchmark tests
|
||||
|
||||
```bash
|
||||
cd parachain/pallets/ethereum-beacon-client
|
||||
cargo test --release --features runtime-benchmarks
|
||||
```
|
||||
@@ -0,0 +1,95 @@
|
||||
[package]
|
||||
name = "snowbridge-ethereum-beacon-client"
|
||||
description = "Snowbridge Beacon Client Pallet"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
repository = "https://github.com/Snowfork/snowbridge"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.188", optional = true }
|
||||
serde_json = { version = "1.0.96", optional = true }
|
||||
codec = { version = "3.6.1", package = "parity-scale-codec", default-features = false, features = ["derive"] }
|
||||
scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
|
||||
ssz_rs = { version = "0.9.0", default-features = false }
|
||||
ssz_rs_derive = { version = "0.9.0", default-features = false }
|
||||
byte-slice-cast = { version = "1.2.1", default-features = false }
|
||||
rlp = { version = "0.5.2", default-features = false }
|
||||
hex-literal = { version = "0.4.1", optional = true }
|
||||
log = { version = "0.4.20", default-features = false }
|
||||
|
||||
frame-benchmarking = { path = "../../../../../substrate/frame/benchmarking", default-features = false, optional = true }
|
||||
frame-support = { path = "../../../../../substrate/frame/support", default-features = false }
|
||||
frame-system = { path = "../../../../../substrate/frame/system", default-features = false }
|
||||
sp-core = { path = "../../../../../substrate/primitives/core", default-features = false }
|
||||
sp-std = { path = "../../../../../substrate/primitives/std", default-features = false }
|
||||
sp-runtime = { path = "../../../../../substrate/primitives/runtime", default-features = false }
|
||||
sp-io = { path = "../../../../../substrate/primitives/io", default-features = false, optional = true }
|
||||
|
||||
snowbridge-core = { path = "../../primitives/core", default-features = false }
|
||||
snowbridge-ethereum = { path = "../../primitives/ethereum", default-features = false }
|
||||
primitives = { package = "snowbridge-beacon-primitives", path = "../../primitives/beacon", default-features = false }
|
||||
static_assertions = { version = "1.1.0", default-features = false }
|
||||
bp-runtime = { path = "../../../../../bridges/primitives/runtime", default-features = false }
|
||||
pallet-timestamp = { path = "../../../../../substrate/frame/timestamp", default-features = false, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8.5"
|
||||
sp-keyring = { path = "../../../../../substrate/primitives/keyring" }
|
||||
serde_json = "1.0.96"
|
||||
hex-literal = "0.4.1"
|
||||
pallet-timestamp = { path = "../../../../../substrate/frame/timestamp" }
|
||||
sp-io = { path = "../../../../../substrate/primitives/io" }
|
||||
serde = "1.0.188"
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
fuzzing = [
|
||||
"hex-literal",
|
||||
"pallet-timestamp",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sp-io",
|
||||
]
|
||||
std = [
|
||||
"bp-runtime/std",
|
||||
"byte-slice-cast/std",
|
||||
"codec/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"log/std",
|
||||
"pallet-timestamp/std",
|
||||
"primitives/std",
|
||||
"rlp/std",
|
||||
"scale-info/std",
|
||||
"serde",
|
||||
"snowbridge-core/std",
|
||||
"snowbridge-ethereum/std",
|
||||
"sp-core/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
"ssz_rs/std",
|
||||
'frame-benchmarking/std',
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"beacon-spec-mainnet",
|
||||
"frame-benchmarking/runtime-benchmarks",
|
||||
"frame-support/runtime-benchmarks",
|
||||
"frame-system/runtime-benchmarks",
|
||||
"hex-literal",
|
||||
"pallet-timestamp?/runtime-benchmarks",
|
||||
"snowbridge-core/runtime-benchmarks",
|
||||
"sp-runtime/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"frame-support/try-runtime",
|
||||
"frame-system/try-runtime",
|
||||
"pallet-timestamp?/try-runtime",
|
||||
"sp-runtime/try-runtime",
|
||||
]
|
||||
beacon-spec-mainnet = []
|
||||
@@ -0,0 +1,88 @@
|
||||
# Motivation
|
||||
Demonstrate that
|
||||
[FastAggregateVerify](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-04#section-3.3.4) is the most
|
||||
expensive call in ethereum beacon light client, though in [#13031](https://github.com/paritytech/substrate/pull/13031)
|
||||
Parity team has wrapped some low level host functions for `bls-12381` but adding a high level host function specific
|
||||
for it is super helpful.
|
||||
|
||||
# Benchmark
|
||||
We add several benchmarks
|
||||
[here](https://github.com/Snowfork/snowbridge/blob/8891ca3cdcf2e04d8118c206588c956541ae4710/parachain/pallets/ethereum-beacon-client/src/benchmarking/mod.rs#L98-L124)
|
||||
as following to demonstrate
|
||||
[bls_fast_aggregate_verify](https://github.com/Snowfork/snowbridge/blob/8891ca3cdcf2e04d8118c206588c956541ae4710/parachain/pallets/ethereum-beacon-client/src/lib.rs#L764)
|
||||
is the main bottleneck. Test data
|
||||
[here](https://github.com/Snowfork/snowbridge/blob/8891ca3cdcf2e04d8118c206588c956541ae4710/parachain/pallets/ethereum-beacon-client/src/benchmarking/data_mainnet.rs#L553-L1120)
|
||||
is real from goerli network which contains 512 public keys from sync committee.
|
||||
|
||||
## sync_committee_period_update
|
||||
Base line benchmark for extrinsic [sync_committee_period_update](https://github.com/Snowfork/snowbridge/blob/8891ca3cdcf2e04d8118c206588c956541ae4710/parachain/pallets/ethereum-beacon-client/src/lib.rs#L233)
|
||||
|
||||
## bls_fast_aggregate_verify
|
||||
Subfunction of extrinsic `sync_committee_period_update` which does what
|
||||
[FastAggregateVerify](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-04#section-3.3.4) requires.
|
||||
|
||||
## bls_aggregate_pubkey
|
||||
Subfunction of `bls_fast_aggregate_verify` which decompress and instantiate G1 pubkeys only.
|
||||
|
||||
## bls_verify_message
|
||||
Subfunction of `bls_fast_aggregate_verify` which verify the prepared signature only.
|
||||
|
||||
|
||||
# Result
|
||||
|
||||
## hardware spec
|
||||
Run benchmark in a EC2 instance
|
||||
```
|
||||
cargo run --release --bin polkadot-parachain --features runtime-benchmarks -- benchmark machine --base-path /mnt/scratch/benchmark
|
||||
|
||||
+----------+----------------+-------------+-------------+-------------------+
|
||||
| Category | Function | Score | Minimum | Result |
|
||||
+===========================================================================+
|
||||
| CPU | BLAKE2-256 | 1.08 GiBs | 1.00 GiBs | ✅ Pass (107.5 %) |
|
||||
|----------+----------------+-------------+-------------+-------------------|
|
||||
| CPU | SR25519-Verify | 568.87 KiBs | 666.00 KiBs | ❌ Fail ( 85.4 %) |
|
||||
|----------+----------------+-------------+-------------+-------------------|
|
||||
| Memory | Copy | 13.67 GiBs | 14.32 GiBs | ✅ Pass ( 95.4 %) |
|
||||
|----------+----------------+-------------+-------------+-------------------|
|
||||
| Disk | Seq Write | 334.35 MiBs | 450.00 MiBs | ❌ Fail ( 74.3 %) |
|
||||
|----------+----------------+-------------+-------------+-------------------|
|
||||
| Disk | Rnd Write | 143.59 MiBs | 200.00 MiBs | ❌ Fail ( 71.8 %) |
|
||||
+----------+----------------+-------------+-------------+-------------------+
|
||||
```
|
||||
|
||||
## benchmark
|
||||
|
||||
```
|
||||
cargo run --release --bin polkadot-parachain \
|
||||
--features runtime-benchmarks \
|
||||
-- \
|
||||
benchmark pallet \
|
||||
--base-path /mnt/scratch/benchmark \
|
||||
--chain=bridge-hub-rococo-dev \
|
||||
--pallet=snowbridge_ethereum_beacon_client \
|
||||
--extrinsic="*" \
|
||||
--execution=wasm --wasm-execution=compiled \
|
||||
--steps 50 --repeat 20 \
|
||||
--output ./parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/snowbridge_ethereum_beacon_client.rs
|
||||
```
|
||||
|
||||
### [Weights](https://github.com/Snowfork/cumulus/blob/ron/benchmark-beacon-bridge/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/snowbridge_ethereum_beacon_client.rs)
|
||||
|
||||
|extrinsic | minimum execution time benchmarked(us) |
|
||||
| --------------------------------------- |----------------------------------------|
|
||||
|sync_committee_period_update | 123_126 |
|
||||
|bls_fast_aggregate_verify| 121_083 |
|
||||
|bls_aggregate_pubkey | 90_306 |
|
||||
|bls_verify_message | 28_000 |
|
||||
|
||||
- [bls_fast_aggregate_verify](#bls_fast_aggregate_verify) consumes 98% execution time of [sync_committee_period_update](#sync_committee_period_update)
|
||||
|
||||
- [bls_aggregate_pubkey](#bls_aggregate_pubkey) consumes 75% execution time of [bls_fast_aggregate_verify](#bls_fast_aggregate_verify)
|
||||
|
||||
- [bls_verify_message](#bls_verify_message) consumes 23% execution time of [bls_fast_aggregate_verify](#bls_fast_aggregate_verify)
|
||||
|
||||
# Conclusion
|
||||
|
||||
A high level host function specific for
|
||||
[bls_fast_aggregate_verify](https://github.com/Snowfork/snowbridge/blob/8891ca3cdcf2e04d8118c206588c956541ae4710/parachain/pallets/ethereum-beacon-client/src/lib.rs#L764)
|
||||
is super helpful.
|
||||
+1215
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,156 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use super::*;
|
||||
|
||||
mod fixtures;
|
||||
mod util;
|
||||
|
||||
use crate::Pallet as EthereumBeaconClient;
|
||||
use frame_benchmarking::v2::*;
|
||||
use frame_system::RawOrigin;
|
||||
|
||||
use fixtures::{
|
||||
make_checkpoint, make_execution_header_update, make_finalized_header_update,
|
||||
make_sync_committee_update,
|
||||
};
|
||||
|
||||
use primitives::{
|
||||
fast_aggregate_verify, prepare_aggregate_pubkey, prepare_aggregate_signature,
|
||||
verify_merkle_branch,
|
||||
};
|
||||
use util::*;
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn force_checkpoint() -> Result<(), BenchmarkError> {
|
||||
let checkpoint_update = make_checkpoint();
|
||||
let block_root: H256 = checkpoint_update.header.hash_tree_root().unwrap();
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, Box::new(*checkpoint_update));
|
||||
|
||||
assert!(<LatestFinalizedBlockRoot<T>>::get() == block_root);
|
||||
assert!(<FinalizedBeaconState<T>>::get(block_root).is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn submit() -> Result<(), BenchmarkError> {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let checkpoint_update = make_checkpoint();
|
||||
let finalized_header_update = make_finalized_header_update();
|
||||
let block_root: H256 = finalized_header_update.finalized_header.hash_tree_root().unwrap();
|
||||
EthereumBeaconClient::<T>::process_checkpoint_update(&checkpoint_update)?;
|
||||
|
||||
#[extrinsic_call]
|
||||
submit(RawOrigin::Signed(caller.clone()), Box::new(*finalized_header_update));
|
||||
|
||||
assert!(<LatestFinalizedBlockRoot<T>>::get() == block_root);
|
||||
assert!(<FinalizedBeaconState<T>>::get(block_root).is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn submit_with_sync_committee() -> Result<(), BenchmarkError> {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let checkpoint_update = make_checkpoint();
|
||||
let sync_committee_update = make_sync_committee_update();
|
||||
EthereumBeaconClient::<T>::process_checkpoint_update(&checkpoint_update)?;
|
||||
|
||||
#[extrinsic_call]
|
||||
submit(RawOrigin::Signed(caller.clone()), Box::new(*sync_committee_update));
|
||||
|
||||
assert!(<NextSyncCommittee<T>>::exists());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn submit_execution_header() -> Result<(), BenchmarkError> {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let checkpoint_update = make_checkpoint();
|
||||
let finalized_header_update = make_finalized_header_update();
|
||||
let execution_header_update = make_execution_header_update();
|
||||
let execution_header_hash = execution_header_update.execution_header.block_hash;
|
||||
EthereumBeaconClient::<T>::process_checkpoint_update(&checkpoint_update)?;
|
||||
EthereumBeaconClient::<T>::process_update(&finalized_header_update)?;
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()), Box::new(*execution_header_update));
|
||||
|
||||
assert!(<ExecutionHeaders<T>>::contains_key(execution_header_hash));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark(extra)]
|
||||
fn bls_fast_aggregate_verify_pre_aggregated() -> Result<(), BenchmarkError> {
|
||||
EthereumBeaconClient::<T>::process_checkpoint_update(&make_checkpoint())?;
|
||||
let update = make_sync_committee_update();
|
||||
let participant_pubkeys = participant_pubkeys::<T>(&update)?;
|
||||
let signing_root = signing_root::<T>(&update)?;
|
||||
let agg_sig =
|
||||
prepare_aggregate_signature(&update.sync_aggregate.sync_committee_signature).unwrap();
|
||||
let agg_pub_key = prepare_aggregate_pubkey(&participant_pubkeys).unwrap();
|
||||
|
||||
#[block]
|
||||
{
|
||||
agg_sig.fast_aggregate_verify_pre_aggregated(signing_root.as_bytes(), &agg_pub_key);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark(extra)]
|
||||
fn bls_fast_aggregate_verify() -> Result<(), BenchmarkError> {
|
||||
EthereumBeaconClient::<T>::process_checkpoint_update(&make_checkpoint())?;
|
||||
let update = make_sync_committee_update();
|
||||
let current_sync_committee = <CurrentSyncCommittee<T>>::get();
|
||||
let absent_pubkeys = absent_pubkeys::<T>(&update)?;
|
||||
let signing_root = signing_root::<T>(&update)?;
|
||||
|
||||
#[block]
|
||||
{
|
||||
fast_aggregate_verify(
|
||||
¤t_sync_committee.aggregate_pubkey,
|
||||
&absent_pubkeys,
|
||||
signing_root,
|
||||
&update.sync_aggregate.sync_committee_signature,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark(extra)]
|
||||
fn verify_merkle_proof() -> Result<(), BenchmarkError> {
|
||||
EthereumBeaconClient::<T>::process_checkpoint_update(&make_checkpoint())?;
|
||||
let update = make_sync_committee_update();
|
||||
let block_root: H256 = update.finalized_header.hash_tree_root().unwrap();
|
||||
|
||||
#[block]
|
||||
{
|
||||
verify_merkle_branch(
|
||||
block_root,
|
||||
&update.finality_branch,
|
||||
config::FINALIZED_ROOT_SUBTREE_INDEX,
|
||||
config::FINALIZED_ROOT_DEPTH,
|
||||
update.attested_header.state_root,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(
|
||||
EthereumBeaconClient,
|
||||
crate::mock::mainnet::new_tester(),
|
||||
crate::mock::mainnet::Test
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use crate::{
|
||||
decompress_sync_committee_bits, Config, CurrentSyncCommittee, Pallet as EthereumBeaconClient,
|
||||
Update, ValidatorsRoot, Vec,
|
||||
};
|
||||
use primitives::PublicKeyPrepared;
|
||||
use sp_core::H256;
|
||||
|
||||
pub fn participant_pubkeys<T: Config>(
|
||||
update: &Update,
|
||||
) -> Result<Vec<PublicKeyPrepared>, &'static str> {
|
||||
let sync_committee_bits =
|
||||
decompress_sync_committee_bits(update.sync_aggregate.sync_committee_bits);
|
||||
let current_sync_committee = <CurrentSyncCommittee<T>>::get();
|
||||
let pubkeys = EthereumBeaconClient::<T>::find_pubkeys(
|
||||
&sync_committee_bits,
|
||||
(*current_sync_committee.pubkeys).as_ref(),
|
||||
true,
|
||||
);
|
||||
Ok(pubkeys)
|
||||
}
|
||||
|
||||
pub fn absent_pubkeys<T: Config>(update: &Update) -> Result<Vec<PublicKeyPrepared>, &'static str> {
|
||||
let sync_committee_bits =
|
||||
decompress_sync_committee_bits(update.sync_aggregate.sync_committee_bits);
|
||||
let current_sync_committee = <CurrentSyncCommittee<T>>::get();
|
||||
let pubkeys = EthereumBeaconClient::<T>::find_pubkeys(
|
||||
&sync_committee_bits,
|
||||
(*current_sync_committee.pubkeys).as_ref(),
|
||||
false,
|
||||
);
|
||||
Ok(pubkeys)
|
||||
}
|
||||
|
||||
pub fn signing_root<T: Config>(update: &Update) -> Result<H256, &'static str> {
|
||||
let validators_root = <ValidatorsRoot<T>>::get();
|
||||
let signing_root = EthereumBeaconClient::<T>::signing_root(
|
||||
&update.attested_header,
|
||||
validators_root,
|
||||
update.signature_slot,
|
||||
)?;
|
||||
Ok(signing_root)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
pub const SLOTS_PER_EPOCH: usize = 32;
|
||||
pub const SECONDS_PER_SLOT: usize = 12;
|
||||
pub const EPOCHS_PER_SYNC_COMMITTEE_PERIOD: usize = 256;
|
||||
pub const SYNC_COMMITTEE_SIZE: usize = 512;
|
||||
pub const SYNC_COMMITTEE_BITS_SIZE: usize = SYNC_COMMITTEE_SIZE / 8;
|
||||
pub const SLOTS_PER_HISTORICAL_ROOT: usize = 8192;
|
||||
pub const IS_MINIMAL: bool = false;
|
||||
pub const BLOCK_ROOT_AT_INDEX_DEPTH: usize = 13;
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
pub const SLOTS_PER_EPOCH: usize = 8;
|
||||
pub const SECONDS_PER_SLOT: usize = 6;
|
||||
pub const EPOCHS_PER_SYNC_COMMITTEE_PERIOD: usize = 8;
|
||||
pub const SYNC_COMMITTEE_SIZE: usize = 32;
|
||||
pub const SYNC_COMMITTEE_BITS_SIZE: usize = SYNC_COMMITTEE_SIZE / 8;
|
||||
pub const SLOTS_PER_HISTORICAL_ROOT: usize = 64;
|
||||
pub const IS_MINIMAL: bool = true;
|
||||
pub const BLOCK_ROOT_AT_INDEX_DEPTH: usize = 6;
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use primitives::merkle_proof::{generalized_index_length, subtree_index};
|
||||
use static_assertions::const_assert;
|
||||
|
||||
pub mod mainnet;
|
||||
pub mod minimal;
|
||||
|
||||
#[cfg(not(feature = "beacon-spec-mainnet"))]
|
||||
pub use minimal::*;
|
||||
|
||||
#[cfg(feature = "beacon-spec-mainnet")]
|
||||
pub use mainnet::*;
|
||||
|
||||
// Generalized Indices
|
||||
|
||||
// get_generalized_index(BeaconState, 'block_roots')
|
||||
pub const BLOCK_ROOTS_INDEX: usize = 37;
|
||||
pub const BLOCK_ROOTS_SUBTREE_INDEX: usize = subtree_index(BLOCK_ROOTS_INDEX);
|
||||
pub const BLOCK_ROOTS_DEPTH: usize = generalized_index_length(BLOCK_ROOTS_INDEX);
|
||||
|
||||
// get_generalized_index(BeaconState, 'finalized_checkpoint', 'root')
|
||||
pub const FINALIZED_ROOT_INDEX: usize = 105;
|
||||
pub const FINALIZED_ROOT_SUBTREE_INDEX: usize = subtree_index(FINALIZED_ROOT_INDEX);
|
||||
pub const FINALIZED_ROOT_DEPTH: usize = generalized_index_length(FINALIZED_ROOT_INDEX);
|
||||
|
||||
// get_generalized_index(BeaconState, 'current_sync_committee')
|
||||
pub const CURRENT_SYNC_COMMITTEE_INDEX: usize = 54;
|
||||
pub const CURRENT_SYNC_COMMITTEE_SUBTREE_INDEX: usize = subtree_index(CURRENT_SYNC_COMMITTEE_INDEX);
|
||||
pub const CURRENT_SYNC_COMMITTEE_DEPTH: usize =
|
||||
generalized_index_length(CURRENT_SYNC_COMMITTEE_INDEX);
|
||||
|
||||
// get_generalized_index(BeaconState, 'next_sync_committee')
|
||||
pub const NEXT_SYNC_COMMITTEE_INDEX: usize = 55;
|
||||
pub const NEXT_SYNC_COMMITTEE_SUBTREE_INDEX: usize = subtree_index(NEXT_SYNC_COMMITTEE_INDEX);
|
||||
pub const NEXT_SYNC_COMMITTEE_DEPTH: usize = generalized_index_length(NEXT_SYNC_COMMITTEE_INDEX);
|
||||
|
||||
// get_generalized_index(BeaconBlockBody, 'execution_payload')
|
||||
pub const EXECUTION_HEADER_INDEX: usize = 25;
|
||||
pub const EXECUTION_HEADER_SUBTREE_INDEX: usize = subtree_index(EXECUTION_HEADER_INDEX);
|
||||
pub const EXECUTION_HEADER_DEPTH: usize = generalized_index_length(EXECUTION_HEADER_INDEX);
|
||||
|
||||
pub const MAX_EXTRA_DATA_BYTES: usize = 32;
|
||||
pub const MAX_LOGS_BLOOM_SIZE: usize = 256;
|
||||
pub const MAX_FEE_RECIPIENT_SIZE: usize = 20;
|
||||
|
||||
pub const MAX_BRANCH_PROOF_SIZE: usize = 20;
|
||||
|
||||
/// DomainType('0x07000000')
|
||||
/// <https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#domain-types>
|
||||
pub const DOMAIN_SYNC_COMMITTEE: [u8; 4] = [7, 0, 0, 0];
|
||||
|
||||
pub const PUBKEY_SIZE: usize = 48;
|
||||
pub const SIGNATURE_SIZE: usize = 96;
|
||||
|
||||
const_assert!(SYNC_COMMITTEE_BITS_SIZE == SYNC_COMMITTEE_SIZE / 8);
|
||||
@@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use crate::config::{
|
||||
EPOCHS_PER_SYNC_COMMITTEE_PERIOD, SLOTS_PER_EPOCH, SYNC_COMMITTEE_BITS_SIZE,
|
||||
SYNC_COMMITTEE_SIZE,
|
||||
};
|
||||
|
||||
/// Decompress packed bitvector into byte vector according to SSZ deserialization rules. Each byte
|
||||
/// in the decompressed vector is either 0 or 1.
|
||||
pub fn decompress_sync_committee_bits(
|
||||
input: [u8; SYNC_COMMITTEE_BITS_SIZE],
|
||||
) -> [u8; SYNC_COMMITTEE_SIZE] {
|
||||
primitives::decompress_sync_committee_bits::<SYNC_COMMITTEE_SIZE, SYNC_COMMITTEE_BITS_SIZE>(
|
||||
input,
|
||||
)
|
||||
}
|
||||
|
||||
/// Compute the sync committee period in which a slot is contained.
|
||||
pub fn compute_period(slot: u64) -> u64 {
|
||||
slot / SLOTS_PER_EPOCH as u64 / EPOCHS_PER_SYNC_COMMITTEE_PERIOD as u64
|
||||
}
|
||||
|
||||
/// Compute epoch in which a slot is contained.
|
||||
pub fn compute_epoch(slot: u64, slots_per_epoch: u64) -> u64 {
|
||||
slot / slots_per_epoch
|
||||
}
|
||||
|
||||
/// Sums the bit vector of sync committee participation.
|
||||
pub fn sync_committee_sum(sync_committee_bits: &[u8]) -> u32 {
|
||||
sync_committee_bits.iter().fold(0, |acc: u32, x| acc + *x as u32)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use super::*;
|
||||
|
||||
use snowbridge_core::inbound::{
|
||||
VerificationError::{self, *},
|
||||
*,
|
||||
};
|
||||
use snowbridge_ethereum::Receipt;
|
||||
|
||||
impl<T: Config> Verifier for Pallet<T> {
|
||||
/// Verify a message by verifying the existence of the corresponding
|
||||
/// Ethereum log in a block. Returns the log if successful. The execution header containing
|
||||
/// the log should be in the beacon client storage, meaning it has been verified and is an
|
||||
/// ancestor of a finalized beacon block.
|
||||
fn verify(event_log: &Log, proof: &Proof) -> Result<(), VerificationError> {
|
||||
log::info!(
|
||||
target: "ethereum-beacon-client",
|
||||
"💫 Verifying message with block hash {}",
|
||||
proof.block_hash,
|
||||
);
|
||||
|
||||
let header = <ExecutionHeaderBuffer<T>>::get(proof.block_hash).ok_or(HeaderNotFound)?;
|
||||
|
||||
let receipt = match Self::verify_receipt_inclusion(header.receipts_root, proof) {
|
||||
Ok(receipt) => receipt,
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
target: "ethereum-beacon-client",
|
||||
"💫 Verification of receipt inclusion failed for block {}: {:?}",
|
||||
proof.block_hash,
|
||||
err
|
||||
);
|
||||
return Err(err)
|
||||
},
|
||||
};
|
||||
|
||||
log::trace!(
|
||||
target: "ethereum-beacon-client",
|
||||
"💫 Verified receipt inclusion for transaction at index {} in block {}",
|
||||
proof.tx_index, proof.block_hash,
|
||||
);
|
||||
|
||||
event_log.validate().map_err(|_| InvalidLog)?;
|
||||
|
||||
// Convert snowbridge_core::inbound::Log to snowbridge_ethereum::Log.
|
||||
let event_log = snowbridge_ethereum::Log {
|
||||
address: event_log.address,
|
||||
topics: event_log.topics.clone(),
|
||||
data: event_log.data.clone(),
|
||||
};
|
||||
|
||||
if !receipt.contains_log(&event_log) {
|
||||
log::error!(
|
||||
target: "ethereum-beacon-client",
|
||||
"💫 Event log not found in receipt for transaction at index {} in block {}",
|
||||
proof.tx_index, proof.block_hash,
|
||||
);
|
||||
return Err(LogNotFound)
|
||||
}
|
||||
|
||||
log::info!(
|
||||
target: "ethereum-beacon-client",
|
||||
"💫 Receipt verification successful for {}",
|
||||
proof.block_hash,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Verifies that the receipt encoded in `proof.data` is included in the block given by
|
||||
/// `proof.block_hash`.
|
||||
pub fn verify_receipt_inclusion(
|
||||
receipts_root: H256,
|
||||
proof: &Proof,
|
||||
) -> Result<Receipt, VerificationError> {
|
||||
let result = verify_receipt_proof(receipts_root, &proof.data.1).ok_or(InvalidProof)?;
|
||||
|
||||
match result {
|
||||
Ok(receipt) => Ok(receipt),
|
||||
Err(err) => {
|
||||
log::trace!(
|
||||
target: "ethereum-beacon-client",
|
||||
"💫 Failed to decode transaction receipt: {}",
|
||||
err
|
||||
);
|
||||
Err(InvalidProof)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,841 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Ethereum Beacon Client
|
||||
//!
|
||||
//! A light client that verifies consensus updates signed by the sync committee of the beacon chain.
|
||||
//!
|
||||
//! # Extrinsics
|
||||
//!
|
||||
//! ## Governance
|
||||
//!
|
||||
//! * [`Call::force_checkpoint`]: Set the initial trusted consensus checkpoint.
|
||||
//! * [`Call::set_operating_mode`]: Set the operating mode of the pallet. Can be used to disable
|
||||
//! processing of conensus updates.
|
||||
//!
|
||||
//! ## Consensus Updates
|
||||
//!
|
||||
//! * [`Call::submit`]: Submit a finalized beacon header with an optional sync committee update
|
||||
//! * [`Call::submit_execution_header`]: Submit an execution header together with an ancestry proof
|
||||
//! that can be verified against an already imported finalized beacon header.
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
pub mod config;
|
||||
pub mod functions;
|
||||
pub mod impls;
|
||||
pub mod types;
|
||||
pub mod weights;
|
||||
|
||||
#[cfg(any(test, feature = "fuzzing"))]
|
||||
pub mod mock;
|
||||
|
||||
#[cfg(all(test, not(feature = "beacon-spec-mainnet")))]
|
||||
mod tests;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
|
||||
use frame_support::{
|
||||
dispatch::DispatchResult, pallet_prelude::OptionQuery, traits::Get, transactional,
|
||||
};
|
||||
use frame_system::ensure_signed;
|
||||
use primitives::{
|
||||
fast_aggregate_verify, verify_merkle_branch, verify_receipt_proof, BeaconHeader, BlsError,
|
||||
CompactBeaconState, CompactExecutionHeader, ExecutionHeaderState, ForkData, ForkVersion,
|
||||
ForkVersions, PublicKeyPrepared, SigningData,
|
||||
};
|
||||
use snowbridge_core::{BasicOperatingMode, RingBufferMap};
|
||||
use sp_core::H256;
|
||||
use sp_std::prelude::*;
|
||||
pub use weights::WeightInfo;
|
||||
|
||||
use functions::{
|
||||
compute_epoch, compute_period, decompress_sync_committee_bits, sync_committee_sum,
|
||||
};
|
||||
pub use types::ExecutionHeaderBuffer;
|
||||
use types::{
|
||||
CheckpointUpdate, ExecutionHeaderUpdate, FinalizedBeaconStateBuffer, SyncCommitteePrepared,
|
||||
Update,
|
||||
};
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
pub use config::SLOTS_PER_HISTORICAL_ROOT;
|
||||
|
||||
pub const LOG_TARGET: &str = "ethereum-beacon-client";
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
|
||||
use frame_support::pallet_prelude::*;
|
||||
use frame_system::pallet_prelude::*;
|
||||
|
||||
#[derive(scale_info::TypeInfo, codec::Encode, codec::Decode, codec::MaxEncodedLen)]
|
||||
#[codec(mel_bound(T: Config))]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct MaxFinalizedHeadersToKeep<T: Config>(PhantomData<T>);
|
||||
impl<T: Config> Get<u32> for MaxFinalizedHeadersToKeep<T> {
|
||||
fn get() -> u32 {
|
||||
// Consider max latency allowed between LatestFinalizedState and LatestExecutionState is
|
||||
// the total slots in one sync_committee_period so 1 should be fine we keep 2 periods
|
||||
// here for redundancy.
|
||||
const MAX_REDUNDANCY: u32 = 2;
|
||||
config::EPOCHS_PER_SYNC_COMMITTEE_PERIOD as u32 * MAX_REDUNDANCY
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config {
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||||
#[pallet::constant]
|
||||
type ForkVersions: Get<ForkVersions>;
|
||||
/// Maximum number of execution headers to keep
|
||||
#[pallet::constant]
|
||||
type MaxExecutionHeadersToKeep: Get<u32>;
|
||||
type WeightInfo: WeightInfo;
|
||||
}
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
BeaconHeaderImported {
|
||||
block_hash: H256,
|
||||
slot: u64,
|
||||
},
|
||||
ExecutionHeaderImported {
|
||||
block_hash: H256,
|
||||
block_number: u64,
|
||||
},
|
||||
SyncCommitteeUpdated {
|
||||
period: u64,
|
||||
},
|
||||
/// Set OperatingMode
|
||||
OperatingModeChanged {
|
||||
mode: BasicOperatingMode,
|
||||
},
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
SkippedSyncCommitteePeriod,
|
||||
/// Attested header is older than latest finalized header.
|
||||
IrrelevantUpdate,
|
||||
NotBootstrapped,
|
||||
SyncCommitteeParticipantsNotSupermajority,
|
||||
InvalidHeaderMerkleProof,
|
||||
InvalidSyncCommitteeMerkleProof,
|
||||
InvalidExecutionHeaderProof,
|
||||
InvalidAncestryMerkleProof,
|
||||
InvalidBlockRootsRootMerkleProof,
|
||||
HeaderNotFinalized,
|
||||
BlockBodyHashTreeRootFailed,
|
||||
HeaderHashTreeRootFailed,
|
||||
SyncCommitteeHashTreeRootFailed,
|
||||
SigningRootHashTreeRootFailed,
|
||||
ForkDataHashTreeRootFailed,
|
||||
ExpectedFinalizedHeaderNotStored,
|
||||
BLSPreparePublicKeysFailed,
|
||||
BLSVerificationFailed(BlsError),
|
||||
InvalidUpdateSlot,
|
||||
/// The given update is not in the expected period, or the given next sync committee does
|
||||
/// not match the next sync committee in storage.
|
||||
InvalidSyncCommitteeUpdate,
|
||||
ExecutionHeaderTooFarBehind,
|
||||
ExecutionHeaderSkippedBlock,
|
||||
Halted,
|
||||
}
|
||||
|
||||
/// Latest imported checkpoint root
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn initial_checkpoint_root)]
|
||||
pub(super) type InitialCheckpointRoot<T: Config> = StorageValue<_, H256, ValueQuery>;
|
||||
|
||||
/// Latest imported finalized block root
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn latest_finalized_block_root)]
|
||||
pub(super) type LatestFinalizedBlockRoot<T: Config> = StorageValue<_, H256, ValueQuery>;
|
||||
|
||||
/// Beacon state by finalized block root
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn finalized_beacon_state)]
|
||||
pub(super) type FinalizedBeaconState<T: Config> =
|
||||
StorageMap<_, Identity, H256, CompactBeaconState, OptionQuery>;
|
||||
|
||||
/// Finalized Headers: Current position in ring buffer
|
||||
#[pallet::storage]
|
||||
pub(crate) type FinalizedBeaconStateIndex<T: Config> = StorageValue<_, u32, ValueQuery>;
|
||||
|
||||
/// Finalized Headers: Mapping of ring buffer index to a pruning candidate
|
||||
#[pallet::storage]
|
||||
pub(crate) type FinalizedBeaconStateMapping<T: Config> =
|
||||
StorageMap<_, Identity, u32, H256, ValueQuery>;
|
||||
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn validators_root)]
|
||||
pub(super) type ValidatorsRoot<T: Config> = StorageValue<_, H256, ValueQuery>;
|
||||
|
||||
/// Sync committee for current period
|
||||
#[pallet::storage]
|
||||
pub(super) type CurrentSyncCommittee<T: Config> =
|
||||
StorageValue<_, SyncCommitteePrepared, ValueQuery>;
|
||||
|
||||
/// Sync committee for next period
|
||||
#[pallet::storage]
|
||||
pub(super) type NextSyncCommittee<T: Config> =
|
||||
StorageValue<_, SyncCommitteePrepared, ValueQuery>;
|
||||
|
||||
/// Latest imported execution header
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn latest_execution_state)]
|
||||
pub(super) type LatestExecutionState<T: Config> =
|
||||
StorageValue<_, ExecutionHeaderState, ValueQuery>;
|
||||
|
||||
/// Execution Headers
|
||||
#[pallet::storage]
|
||||
pub type ExecutionHeaders<T: Config> =
|
||||
StorageMap<_, Identity, H256, CompactExecutionHeader, OptionQuery>;
|
||||
|
||||
/// Execution Headers: Current position in ring buffer
|
||||
#[pallet::storage]
|
||||
pub type ExecutionHeaderIndex<T: Config> = StorageValue<_, u32, ValueQuery>;
|
||||
|
||||
/// Execution Headers: Mapping of ring buffer index to a pruning candidate
|
||||
#[pallet::storage]
|
||||
pub type ExecutionHeaderMapping<T: Config> = StorageMap<_, Identity, u32, H256, ValueQuery>;
|
||||
|
||||
/// The current operating mode of the pallet.
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn operating_mode)]
|
||||
pub type OperatingMode<T: Config> = StorageValue<_, BasicOperatingMode, ValueQuery>;
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(T::WeightInfo::force_checkpoint())]
|
||||
#[transactional]
|
||||
/// Used for pallet initialization and light client resetting. Needs to be called by
|
||||
/// the root origin.
|
||||
pub fn force_checkpoint(
|
||||
origin: OriginFor<T>,
|
||||
update: Box<CheckpointUpdate>,
|
||||
) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
Self::process_checkpoint_update(&update)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight({
|
||||
match update.next_sync_committee_update {
|
||||
None => T::WeightInfo::submit(),
|
||||
Some(_) => T::WeightInfo::submit_with_sync_committee(),
|
||||
}
|
||||
})]
|
||||
#[transactional]
|
||||
/// Submits a new finalized beacon header update. The update may contain the next
|
||||
/// sync committee.
|
||||
pub fn submit(origin: OriginFor<T>, update: Box<Update>) -> DispatchResult {
|
||||
ensure_signed(origin)?;
|
||||
ensure!(!Self::operating_mode().is_halted(), Error::<T>::Halted);
|
||||
Self::process_update(&update)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pallet::call_index(2)]
|
||||
#[pallet::weight(T::WeightInfo::submit_execution_header())]
|
||||
#[transactional]
|
||||
/// Submits a new execution header update. The relevant related beacon header
|
||||
/// is also included to prove the execution header, as well as ancestry proof data.
|
||||
pub fn submit_execution_header(
|
||||
origin: OriginFor<T>,
|
||||
update: Box<ExecutionHeaderUpdate>,
|
||||
) -> DispatchResult {
|
||||
ensure_signed(origin)?;
|
||||
ensure!(!Self::operating_mode().is_halted(), Error::<T>::Halted);
|
||||
Self::process_execution_header_update(&update)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Halt or resume all pallet operations. May only be called by root.
|
||||
#[pallet::call_index(3)]
|
||||
#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
|
||||
pub fn set_operating_mode(
|
||||
origin: OriginFor<T>,
|
||||
mode: BasicOperatingMode,
|
||||
) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
OperatingMode::<T>::set(mode);
|
||||
Self::deposit_event(Event::OperatingModeChanged { mode });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Forces a finalized beacon header checkpoint update. The current sync committee,
|
||||
/// with a header attesting to the current sync committee, should be provided.
|
||||
/// An `block_roots` proof should also be provided. This is used for ancestry proofs
|
||||
/// for execution header updates.
|
||||
pub(crate) fn process_checkpoint_update(update: &CheckpointUpdate) -> DispatchResult {
|
||||
let sync_committee_root = update
|
||||
.current_sync_committee
|
||||
.hash_tree_root()
|
||||
.map_err(|_| Error::<T>::SyncCommitteeHashTreeRootFailed)?;
|
||||
|
||||
// Verifies the sync committee in the Beacon state.
|
||||
ensure!(
|
||||
verify_merkle_branch(
|
||||
sync_committee_root,
|
||||
&update.current_sync_committee_branch,
|
||||
config::CURRENT_SYNC_COMMITTEE_SUBTREE_INDEX,
|
||||
config::CURRENT_SYNC_COMMITTEE_DEPTH,
|
||||
update.header.state_root
|
||||
),
|
||||
Error::<T>::InvalidSyncCommitteeMerkleProof
|
||||
);
|
||||
|
||||
let header_root: H256 = update
|
||||
.header
|
||||
.hash_tree_root()
|
||||
.map_err(|_| Error::<T>::HeaderHashTreeRootFailed)?;
|
||||
|
||||
// This is used for ancestry proofs in ExecutionHeader updates. This verifies the
|
||||
// BeaconState: the beacon state root is the tree root; the `block_roots` hash is the
|
||||
// tree leaf.
|
||||
ensure!(
|
||||
verify_merkle_branch(
|
||||
update.block_roots_root,
|
||||
&update.block_roots_branch,
|
||||
config::BLOCK_ROOTS_SUBTREE_INDEX,
|
||||
config::BLOCK_ROOTS_DEPTH,
|
||||
update.header.state_root
|
||||
),
|
||||
Error::<T>::InvalidBlockRootsRootMerkleProof
|
||||
);
|
||||
|
||||
let sync_committee_prepared: SyncCommitteePrepared = (&update.current_sync_committee)
|
||||
.try_into()
|
||||
.map_err(|_| <Error<T>>::BLSPreparePublicKeysFailed)?;
|
||||
<CurrentSyncCommittee<T>>::set(sync_committee_prepared);
|
||||
<NextSyncCommittee<T>>::kill();
|
||||
InitialCheckpointRoot::<T>::set(header_root);
|
||||
<LatestExecutionState<T>>::kill();
|
||||
|
||||
Self::store_validators_root(update.validators_root);
|
||||
Self::store_finalized_header(header_root, update.header, update.block_roots_root)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn process_update(update: &Update) -> DispatchResult {
|
||||
Self::cross_check_execution_state()?;
|
||||
Self::verify_update(update)?;
|
||||
Self::apply_update(update)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cross check to make sure that execution header import does not fall too far behind
|
||||
/// finalised beacon header import. If that happens just return an error and pause
|
||||
/// processing until execution header processing has caught up.
|
||||
pub(crate) fn cross_check_execution_state() -> DispatchResult {
|
||||
let latest_finalized_state =
|
||||
FinalizedBeaconState::<T>::get(LatestFinalizedBlockRoot::<T>::get())
|
||||
.ok_or(Error::<T>::NotBootstrapped)?;
|
||||
let latest_execution_state = Self::latest_execution_state();
|
||||
// The execution header import should be at least within the slot range of a sync
|
||||
// committee period.
|
||||
let max_latency = config::EPOCHS_PER_SYNC_COMMITTEE_PERIOD * config::SLOTS_PER_EPOCH;
|
||||
ensure!(
|
||||
latest_execution_state.beacon_slot == 0 ||
|
||||
latest_finalized_state.slot <
|
||||
latest_execution_state.beacon_slot + max_latency as u64,
|
||||
Error::<T>::ExecutionHeaderTooFarBehind
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// References and strictly follows <https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#validate_light_client_update>
|
||||
/// Verifies that provided next sync committee is valid through a series of checks
|
||||
/// (including checking that a sync committee period isn't skipped and that the header is
|
||||
/// signed by the current sync committee.
|
||||
fn verify_update(update: &Update) -> DispatchResult {
|
||||
// Verify sync committee has sufficient participants.
|
||||
let participation =
|
||||
decompress_sync_committee_bits(update.sync_aggregate.sync_committee_bits);
|
||||
Self::sync_committee_participation_is_supermajority(&participation)?;
|
||||
|
||||
// Verify update does not skip a sync committee period.
|
||||
ensure!(
|
||||
update.signature_slot > update.attested_header.slot &&
|
||||
update.attested_header.slot >= update.finalized_header.slot,
|
||||
Error::<T>::InvalidUpdateSlot
|
||||
);
|
||||
// Retrieve latest finalized state.
|
||||
let latest_finalized_state =
|
||||
FinalizedBeaconState::<T>::get(LatestFinalizedBlockRoot::<T>::get())
|
||||
.ok_or(Error::<T>::NotBootstrapped)?;
|
||||
let store_period = compute_period(latest_finalized_state.slot);
|
||||
let signature_period = compute_period(update.signature_slot);
|
||||
if <NextSyncCommittee<T>>::exists() {
|
||||
ensure!(
|
||||
(store_period..=store_period + 1).contains(&signature_period),
|
||||
Error::<T>::SkippedSyncCommitteePeriod
|
||||
)
|
||||
} else {
|
||||
ensure!(signature_period == store_period, Error::<T>::SkippedSyncCommitteePeriod)
|
||||
}
|
||||
|
||||
// Verify update is relevant.
|
||||
let update_attested_period = compute_period(update.attested_header.slot);
|
||||
let update_has_next_sync_committee = !<NextSyncCommittee<T>>::exists() &&
|
||||
(update.next_sync_committee_update.is_some() &&
|
||||
update_attested_period == store_period);
|
||||
ensure!(
|
||||
update.attested_header.slot > latest_finalized_state.slot ||
|
||||
update_has_next_sync_committee,
|
||||
Error::<T>::IrrelevantUpdate
|
||||
);
|
||||
|
||||
// Verify that the `finality_branch`, if present, confirms `finalized_header` to match
|
||||
// the finalized checkpoint root saved in the state of `attested_header`.
|
||||
let finalized_block_root: H256 = update
|
||||
.finalized_header
|
||||
.hash_tree_root()
|
||||
.map_err(|_| Error::<T>::HeaderHashTreeRootFailed)?;
|
||||
ensure!(
|
||||
verify_merkle_branch(
|
||||
finalized_block_root,
|
||||
&update.finality_branch,
|
||||
config::FINALIZED_ROOT_SUBTREE_INDEX,
|
||||
config::FINALIZED_ROOT_DEPTH,
|
||||
update.attested_header.state_root
|
||||
),
|
||||
Error::<T>::InvalidHeaderMerkleProof
|
||||
);
|
||||
|
||||
// Though following check does not belong to ALC spec we verify block_roots_root to
|
||||
// match the finalized checkpoint root saved in the state of `finalized_header` so to
|
||||
// cache it for later use in `verify_ancestry_proof`.
|
||||
ensure!(
|
||||
verify_merkle_branch(
|
||||
update.block_roots_root,
|
||||
&update.block_roots_branch,
|
||||
config::BLOCK_ROOTS_SUBTREE_INDEX,
|
||||
config::BLOCK_ROOTS_DEPTH,
|
||||
update.finalized_header.state_root
|
||||
),
|
||||
Error::<T>::InvalidBlockRootsRootMerkleProof
|
||||
);
|
||||
|
||||
// Verify that the `next_sync_committee`, if present, actually is the next sync
|
||||
// committee saved in the state of the `attested_header`.
|
||||
if let Some(next_sync_committee_update) = &update.next_sync_committee_update {
|
||||
let sync_committee_root = next_sync_committee_update
|
||||
.next_sync_committee
|
||||
.hash_tree_root()
|
||||
.map_err(|_| Error::<T>::SyncCommitteeHashTreeRootFailed)?;
|
||||
if update_attested_period == store_period && <NextSyncCommittee<T>>::exists() {
|
||||
let next_committee_root = <NextSyncCommittee<T>>::get().root;
|
||||
ensure!(
|
||||
sync_committee_root == next_committee_root,
|
||||
Error::<T>::InvalidSyncCommitteeUpdate
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
verify_merkle_branch(
|
||||
sync_committee_root,
|
||||
&next_sync_committee_update.next_sync_committee_branch,
|
||||
config::NEXT_SYNC_COMMITTEE_SUBTREE_INDEX,
|
||||
config::NEXT_SYNC_COMMITTEE_DEPTH,
|
||||
update.attested_header.state_root
|
||||
),
|
||||
Error::<T>::InvalidSyncCommitteeMerkleProof
|
||||
);
|
||||
}
|
||||
|
||||
// Verify sync committee aggregate signature.
|
||||
let sync_committee = if signature_period == store_period {
|
||||
<CurrentSyncCommittee<T>>::get()
|
||||
} else {
|
||||
<NextSyncCommittee<T>>::get()
|
||||
};
|
||||
let absent_pubkeys =
|
||||
Self::find_pubkeys(&participation, (*sync_committee.pubkeys).as_ref(), false);
|
||||
let signing_root = Self::signing_root(
|
||||
&update.attested_header,
|
||||
Self::validators_root(),
|
||||
update.signature_slot,
|
||||
)?;
|
||||
// Improvement here per <https://eth2book.info/capella/part2/building_blocks/signatures/#sync-aggregates>
|
||||
// suggested start from the full set aggregate_pubkey then subtracting the absolute
|
||||
// minority that did not participate.
|
||||
fast_aggregate_verify(
|
||||
&sync_committee.aggregate_pubkey,
|
||||
&absent_pubkeys,
|
||||
signing_root,
|
||||
&update.sync_aggregate.sync_committee_signature,
|
||||
)
|
||||
.map_err(|e| Error::<T>::BLSVerificationFailed(e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reference and strictly follows <https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#apply_light_client_update
|
||||
/// Applies a finalized beacon header update to the beacon client. If a next sync committee
|
||||
/// is present in the update, verify the sync committee by converting it to a
|
||||
/// SyncCommitteePrepared type. Stores the provided finalized header.
|
||||
fn apply_update(update: &Update) -> DispatchResult {
|
||||
let latest_finalized_state =
|
||||
FinalizedBeaconState::<T>::get(LatestFinalizedBlockRoot::<T>::get())
|
||||
.ok_or(Error::<T>::NotBootstrapped)?;
|
||||
if let Some(next_sync_committee_update) = &update.next_sync_committee_update {
|
||||
let store_period = compute_period(latest_finalized_state.slot);
|
||||
let update_finalized_period = compute_period(update.finalized_header.slot);
|
||||
let sync_committee_prepared: SyncCommitteePrepared = (&next_sync_committee_update
|
||||
.next_sync_committee)
|
||||
.try_into()
|
||||
.map_err(|_| <Error<T>>::BLSPreparePublicKeysFailed)?;
|
||||
|
||||
if !<NextSyncCommittee<T>>::exists() {
|
||||
ensure!(
|
||||
update_finalized_period == store_period,
|
||||
<Error<T>>::InvalidSyncCommitteeUpdate
|
||||
);
|
||||
<NextSyncCommittee<T>>::set(sync_committee_prepared);
|
||||
} else if update_finalized_period == store_period + 1 {
|
||||
<CurrentSyncCommittee<T>>::set(<NextSyncCommittee<T>>::get());
|
||||
<NextSyncCommittee<T>>::set(sync_committee_prepared);
|
||||
}
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"💫 SyncCommitteeUpdated at period {}.",
|
||||
update_finalized_period
|
||||
);
|
||||
Self::deposit_event(Event::SyncCommitteeUpdated {
|
||||
period: update_finalized_period,
|
||||
});
|
||||
};
|
||||
|
||||
if update.finalized_header.slot > latest_finalized_state.slot {
|
||||
let finalized_block_root: H256 = update
|
||||
.finalized_header
|
||||
.hash_tree_root()
|
||||
.map_err(|_| Error::<T>::HeaderHashTreeRootFailed)?;
|
||||
Self::store_finalized_header(
|
||||
finalized_block_root,
|
||||
update.finalized_header,
|
||||
update.block_roots_root,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validates an execution header for import. The beacon header containing the execution
|
||||
/// header is sent, plus the execution header, along with a proof that the execution header
|
||||
/// is rooted in the beacon header body.
|
||||
pub(crate) fn process_execution_header_update(
|
||||
update: &ExecutionHeaderUpdate,
|
||||
) -> DispatchResult {
|
||||
let latest_finalized_state =
|
||||
FinalizedBeaconState::<T>::get(LatestFinalizedBlockRoot::<T>::get())
|
||||
.ok_or(Error::<T>::NotBootstrapped)?;
|
||||
// Checks that the header is an ancestor of a finalized header, using slot number.
|
||||
ensure!(
|
||||
update.header.slot <= latest_finalized_state.slot,
|
||||
Error::<T>::HeaderNotFinalized
|
||||
);
|
||||
|
||||
// Checks that we don't skip execution headers, they need to be imported sequentially.
|
||||
let latest_execution_state: ExecutionHeaderState = Self::latest_execution_state();
|
||||
ensure!(
|
||||
latest_execution_state.block_number == 0 ||
|
||||
update.execution_header.block_number ==
|
||||
latest_execution_state.block_number + 1,
|
||||
Error::<T>::ExecutionHeaderSkippedBlock
|
||||
);
|
||||
|
||||
// Gets the hash tree root of the execution header, in preparation for the execution
|
||||
// header proof (used to check that the execution header is rooted in the beacon
|
||||
// header body.
|
||||
let execution_header_root: H256 = update
|
||||
.execution_header
|
||||
.hash_tree_root()
|
||||
.map_err(|_| Error::<T>::BlockBodyHashTreeRootFailed)?;
|
||||
|
||||
ensure!(
|
||||
verify_merkle_branch(
|
||||
execution_header_root,
|
||||
&update.execution_branch,
|
||||
config::EXECUTION_HEADER_SUBTREE_INDEX,
|
||||
config::EXECUTION_HEADER_DEPTH,
|
||||
update.header.body_root
|
||||
),
|
||||
Error::<T>::InvalidExecutionHeaderProof
|
||||
);
|
||||
|
||||
let block_root: H256 = update
|
||||
.header
|
||||
.hash_tree_root()
|
||||
.map_err(|_| Error::<T>::HeaderHashTreeRootFailed)?;
|
||||
|
||||
match &update.ancestry_proof {
|
||||
Some(proof) => {
|
||||
Self::verify_ancestry_proof(
|
||||
block_root,
|
||||
update.header.slot,
|
||||
&proof.header_branch,
|
||||
proof.finalized_block_root,
|
||||
)?;
|
||||
},
|
||||
None => {
|
||||
// If the ancestry proof is not provided, we expect this header to be a
|
||||
// finalized header. We need to check that the header hash matches the finalized
|
||||
// header root at the expected slot.
|
||||
let state = <FinalizedBeaconState<T>>::get(block_root)
|
||||
.ok_or(Error::<T>::ExpectedFinalizedHeaderNotStored)?;
|
||||
if update.header.slot != state.slot {
|
||||
return Err(Error::<T>::ExpectedFinalizedHeaderNotStored.into())
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Self::store_execution_header(
|
||||
update.execution_header.block_hash,
|
||||
update.execution_header.clone().into(),
|
||||
update.header.slot,
|
||||
block_root,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify that `block_root` is an ancestor of `finalized_block_root` Used to prove that
|
||||
/// an execution header is an ancestor of a finalized header (i.e. the blocks are
|
||||
/// on the same chain).
|
||||
fn verify_ancestry_proof(
|
||||
block_root: H256,
|
||||
block_slot: u64,
|
||||
block_root_proof: &[H256],
|
||||
finalized_block_root: H256,
|
||||
) -> DispatchResult {
|
||||
let state = <FinalizedBeaconState<T>>::get(finalized_block_root)
|
||||
.ok_or(Error::<T>::ExpectedFinalizedHeaderNotStored)?;
|
||||
|
||||
ensure!(block_slot < state.slot, Error::<T>::HeaderNotFinalized);
|
||||
|
||||
let index_in_array = block_slot % (SLOTS_PER_HISTORICAL_ROOT as u64);
|
||||
let leaf_index = (SLOTS_PER_HISTORICAL_ROOT as u64) + index_in_array;
|
||||
|
||||
ensure!(
|
||||
verify_merkle_branch(
|
||||
block_root,
|
||||
block_root_proof,
|
||||
leaf_index as usize,
|
||||
config::BLOCK_ROOT_AT_INDEX_DEPTH,
|
||||
state.block_roots_root
|
||||
),
|
||||
Error::<T>::InvalidAncestryMerkleProof
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Computes the signing root for a given beacon header and domain. The hash tree root
|
||||
/// of the beacon header is computed, and then the combination of the beacon header hash
|
||||
/// and the domain makes up the signing root.
|
||||
pub(super) fn compute_signing_root(
|
||||
beacon_header: &BeaconHeader,
|
||||
domain: H256,
|
||||
) -> Result<H256, DispatchError> {
|
||||
let beacon_header_root = beacon_header
|
||||
.hash_tree_root()
|
||||
.map_err(|_| Error::<T>::HeaderHashTreeRootFailed)?;
|
||||
|
||||
let hash_root = SigningData { object_root: beacon_header_root, domain }
|
||||
.hash_tree_root()
|
||||
.map_err(|_| Error::<T>::SigningRootHashTreeRootFailed)?;
|
||||
|
||||
Ok(hash_root)
|
||||
}
|
||||
|
||||
/// Stores a compacted (slot and block roots root (hash of the `block_roots` beacon state
|
||||
/// field, used for ancestry proof)) beacon state in a ring buffer map, with the header root
|
||||
/// as map key.
|
||||
fn store_finalized_header(
|
||||
header_root: H256,
|
||||
header: BeaconHeader,
|
||||
block_roots_root: H256,
|
||||
) -> DispatchResult {
|
||||
let slot = header.slot;
|
||||
|
||||
<FinalizedBeaconStateBuffer<T>>::insert(
|
||||
header_root,
|
||||
CompactBeaconState { slot: header.slot, block_roots_root },
|
||||
);
|
||||
<LatestFinalizedBlockRoot<T>>::set(header_root);
|
||||
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"💫 Updated latest finalized block root {} at slot {}.",
|
||||
header_root,
|
||||
slot
|
||||
);
|
||||
|
||||
Self::deposit_event(Event::BeaconHeaderImported { block_hash: header_root, slot });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stores the provided execution header in pallet storage. The header is stored
|
||||
/// in a ring buffer map, with the block hash as map key. The last imported execution
|
||||
/// header is also kept in storage, for the relayer to check import progress.
|
||||
pub(crate) fn store_execution_header(
|
||||
block_hash: H256,
|
||||
header: CompactExecutionHeader,
|
||||
beacon_slot: u64,
|
||||
beacon_block_root: H256,
|
||||
) {
|
||||
let block_number = header.block_number;
|
||||
|
||||
<ExecutionHeaderBuffer<T>>::insert(block_hash, header);
|
||||
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"💫 Updated latest execution block at {} to number {}.",
|
||||
block_hash,
|
||||
block_number
|
||||
);
|
||||
|
||||
LatestExecutionState::<T>::mutate(|s| {
|
||||
s.beacon_block_root = beacon_block_root;
|
||||
s.beacon_slot = beacon_slot;
|
||||
s.block_hash = block_hash;
|
||||
s.block_number = block_number;
|
||||
});
|
||||
|
||||
Self::deposit_event(Event::ExecutionHeaderImported { block_hash, block_number });
|
||||
}
|
||||
|
||||
/// Stores the validators root in storage. Validators root is the hash tree root of all the
|
||||
/// validators at genesis and is used to used to identify the chain that we are on
|
||||
/// (used in conjunction with the fork version).
|
||||
/// <https://eth2book.info/capella/part3/containers/state/#genesis_validators_root>
|
||||
fn store_validators_root(validators_root: H256) {
|
||||
<ValidatorsRoot<T>>::set(validators_root);
|
||||
}
|
||||
|
||||
/// Returns the domain for the domain_type and fork_version. The domain is used to
|
||||
/// distinguish between the different players in the chain (see DomainTypes
|
||||
/// <https://eth2book.info/capella/part3/config/constants/#domain-types>) and to ensure we are
|
||||
/// addressing the correct chain.
|
||||
/// <https://eth2book.info/capella/part3/helper/misc/#compute_domain>
|
||||
pub(super) fn compute_domain(
|
||||
domain_type: Vec<u8>,
|
||||
fork_version: ForkVersion,
|
||||
genesis_validators_root: H256,
|
||||
) -> Result<H256, DispatchError> {
|
||||
let fork_data_root =
|
||||
Self::compute_fork_data_root(fork_version, genesis_validators_root)?;
|
||||
|
||||
let mut domain = [0u8; 32];
|
||||
domain[0..4].copy_from_slice(&(domain_type));
|
||||
domain[4..32].copy_from_slice(&(fork_data_root.0[..28]));
|
||||
|
||||
Ok(domain.into())
|
||||
}
|
||||
|
||||
/// Computes the fork data root. The fork data root is a merkleization of the current
|
||||
/// fork version and the genesis validators root.
|
||||
fn compute_fork_data_root(
|
||||
current_version: ForkVersion,
|
||||
genesis_validators_root: H256,
|
||||
) -> Result<H256, DispatchError> {
|
||||
let hash_root = ForkData {
|
||||
current_version,
|
||||
genesis_validators_root: genesis_validators_root.into(),
|
||||
}
|
||||
.hash_tree_root()
|
||||
.map_err(|_| Error::<T>::ForkDataHashTreeRootFailed)?;
|
||||
|
||||
Ok(hash_root)
|
||||
}
|
||||
|
||||
/// Checks that the sync committee bits (the votes of the sync committee members,
|
||||
/// represented by bits 0 and 1) is more than a supermajority (2/3 of the votes are
|
||||
/// positive).
|
||||
pub(super) fn sync_committee_participation_is_supermajority(
|
||||
sync_committee_bits: &[u8],
|
||||
) -> DispatchResult {
|
||||
let sync_committee_sum = sync_committee_sum(sync_committee_bits);
|
||||
ensure!(
|
||||
((sync_committee_sum * 3) as usize) >= sync_committee_bits.len() * 2,
|
||||
Error::<T>::SyncCommitteeParticipantsNotSupermajority
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the fork version based on the current epoch. The hard fork versions
|
||||
/// are defined in pallet config.
|
||||
pub(super) fn compute_fork_version(epoch: u64) -> ForkVersion {
|
||||
Self::select_fork_version(&T::ForkVersions::get(), epoch)
|
||||
}
|
||||
|
||||
/// Returns the fork version based on the current epoch.
|
||||
pub(super) fn select_fork_version(fork_versions: &ForkVersions, epoch: u64) -> ForkVersion {
|
||||
if epoch >= fork_versions.capella.epoch {
|
||||
return fork_versions.capella.version
|
||||
}
|
||||
if epoch >= fork_versions.bellatrix.epoch {
|
||||
return fork_versions.bellatrix.version
|
||||
}
|
||||
if epoch >= fork_versions.altair.epoch {
|
||||
return fork_versions.altair.version
|
||||
}
|
||||
|
||||
fork_versions.genesis.version
|
||||
}
|
||||
|
||||
/// Returns a vector of public keys that participated in the sync committee block signage.
|
||||
/// Sync committee bits is an array of 0s and 1s, 0 meaning the corresponding sync committee
|
||||
/// member did not participate in the vote, 1 meaning they participated.
|
||||
/// This method can find the absent or participating members, based on the participant
|
||||
/// parameter. participant = false will return absent participants, participant = true will
|
||||
/// return participating members.
|
||||
pub fn find_pubkeys(
|
||||
sync_committee_bits: &[u8],
|
||||
sync_committee_pubkeys: &[PublicKeyPrepared],
|
||||
participant: bool,
|
||||
) -> Vec<PublicKeyPrepared> {
|
||||
let mut pubkeys: Vec<PublicKeyPrepared> = Vec::new();
|
||||
for (bit, pubkey) in sync_committee_bits.iter().zip(sync_committee_pubkeys.iter()) {
|
||||
if *bit == u8::from(participant) {
|
||||
pubkeys.push(*pubkey);
|
||||
}
|
||||
}
|
||||
pubkeys
|
||||
}
|
||||
|
||||
/// Calculates signing root for BeaconHeader. The signing root is used for the message
|
||||
/// value in BLS signature verification.
|
||||
pub fn signing_root(
|
||||
header: &BeaconHeader,
|
||||
validators_root: H256,
|
||||
signature_slot: u64,
|
||||
) -> Result<H256, DispatchError> {
|
||||
let fork_version = Self::compute_fork_version(compute_epoch(
|
||||
signature_slot,
|
||||
config::SLOTS_PER_EPOCH as u64,
|
||||
));
|
||||
let domain_type = config::DOMAIN_SYNC_COMMITTEE.to_vec();
|
||||
// Domains are used for for seeds, for signatures, and for selecting aggregators.
|
||||
let domain = Self::compute_domain(domain_type, fork_version, validators_root)?;
|
||||
// Hash tree root of SigningData - object root + domain
|
||||
let signing_root = Self::compute_signing_root(header, domain)?;
|
||||
Ok(signing_root)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use crate as ethereum_beacon_client;
|
||||
use frame_support::parameter_types;
|
||||
use pallet_timestamp;
|
||||
use primitives::{Fork, ForkVersions};
|
||||
use sp_core::H256;
|
||||
use sp_runtime::traits::{BlakeTwo256, IdentityLookup};
|
||||
|
||||
#[cfg(not(feature = "beacon-spec-mainnet"))]
|
||||
pub mod minimal {
|
||||
use super::*;
|
||||
|
||||
use crate::config;
|
||||
use hex_literal::hex;
|
||||
use primitives::CompactExecutionHeader;
|
||||
use snowbridge_core::inbound::{Log, Proof};
|
||||
use sp_runtime::BuildStorage;
|
||||
use std::{fs::File, path::PathBuf};
|
||||
|
||||
type Block = frame_system::mocking::MockBlock<Test>;
|
||||
|
||||
frame_support::construct_runtime!(
|
||||
pub enum Test {
|
||||
System: frame_system::{Pallet, Call, Storage, Event<T>},
|
||||
Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent},
|
||||
EthereumBeaconClient: ethereum_beacon_client::{Pallet, Call, Storage, Event<T>},
|
||||
}
|
||||
);
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
pub const SS58Prefix: u8 = 42;
|
||||
}
|
||||
|
||||
impl frame_system::Config for Test {
|
||||
type BaseCallFilter = frame_support::traits::Everything;
|
||||
type OnSetCode = ();
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type RuntimeTask = RuntimeTask;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = u64;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = ();
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = SS58Prefix;
|
||||
type MaxConsumers = frame_support::traits::ConstU32<16>;
|
||||
type Nonce = u64;
|
||||
type Block = Block;
|
||||
}
|
||||
|
||||
impl pallet_timestamp::Config for Test {
|
||||
type Moment = u64;
|
||||
type OnTimestampSet = ();
|
||||
type MinimumPeriod = ();
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const ExecutionHeadersPruneThreshold: u32 = 10;
|
||||
pub const ChainForkVersions: ForkVersions = ForkVersions{
|
||||
genesis: Fork {
|
||||
version: [0, 0, 0, 1], // 0x00000001
|
||||
epoch: 0,
|
||||
},
|
||||
altair: Fork {
|
||||
version: [1, 0, 0, 1], // 0x01000001
|
||||
epoch: 0,
|
||||
},
|
||||
bellatrix: Fork {
|
||||
version: [2, 0, 0, 1], // 0x02000001
|
||||
epoch: 0,
|
||||
},
|
||||
capella: Fork {
|
||||
version: [3, 0, 0, 1], // 0x03000001
|
||||
epoch: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
impl ethereum_beacon_client::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type ForkVersions = ChainForkVersions;
|
||||
type MaxExecutionHeadersToKeep = ExecutionHeadersPruneThreshold;
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
// Build genesis storage according to the mock runtime.
|
||||
pub fn new_tester() -> sp_io::TestExternalities {
|
||||
let t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
let mut ext = sp_io::TestExternalities::new(t);
|
||||
let _ = ext.execute_with(|| Timestamp::set(RuntimeOrigin::signed(1), 30_000));
|
||||
ext
|
||||
}
|
||||
|
||||
fn load_fixture<T>(basename: &str) -> Result<T, serde_json::Error>
|
||||
where
|
||||
T: for<'de> serde::Deserialize<'de>,
|
||||
{
|
||||
let filepath: PathBuf =
|
||||
[env!("CARGO_MANIFEST_DIR"), "tests", "fixtures", basename].iter().collect();
|
||||
serde_json::from_reader(File::open(filepath).unwrap())
|
||||
}
|
||||
|
||||
pub fn load_execution_header_update_fixture() -> primitives::ExecutionHeaderUpdate {
|
||||
load_fixture("execution-header-update.minimal.json").unwrap()
|
||||
}
|
||||
|
||||
pub fn load_checkpoint_update_fixture(
|
||||
) -> primitives::CheckpointUpdate<{ config::SYNC_COMMITTEE_SIZE }> {
|
||||
load_fixture("initial-checkpoint.minimal.json").unwrap()
|
||||
}
|
||||
|
||||
pub fn load_sync_committee_update_fixture(
|
||||
) -> primitives::Update<{ config::SYNC_COMMITTEE_SIZE }, { config::SYNC_COMMITTEE_BITS_SIZE }> {
|
||||
load_fixture("sync-committee-update.minimal.json").unwrap()
|
||||
}
|
||||
|
||||
pub fn load_finalized_header_update_fixture(
|
||||
) -> primitives::Update<{ config::SYNC_COMMITTEE_SIZE }, { config::SYNC_COMMITTEE_BITS_SIZE }> {
|
||||
load_fixture("finalized-header-update.minimal.json").unwrap()
|
||||
}
|
||||
|
||||
pub fn load_next_sync_committee_update_fixture(
|
||||
) -> primitives::Update<{ config::SYNC_COMMITTEE_SIZE }, { config::SYNC_COMMITTEE_BITS_SIZE }> {
|
||||
load_fixture("next-sync-committee-update.minimal.json").unwrap()
|
||||
}
|
||||
|
||||
pub fn load_next_finalized_header_update_fixture(
|
||||
) -> primitives::Update<{ config::SYNC_COMMITTEE_SIZE }, { config::SYNC_COMMITTEE_BITS_SIZE }> {
|
||||
load_fixture("next-finalized-header-update.minimal.json").unwrap()
|
||||
}
|
||||
|
||||
pub fn get_message_verification_payload() -> (Log, Proof) {
|
||||
(
|
||||
Log {
|
||||
address: hex!("ee9170abfbf9421ad6dd07f6bdec9d89f2b581e0").into(),
|
||||
topics: vec![
|
||||
hex!("1b11dcf133cc240f682dab2d3a8e4cd35c5da8c9cf99adac4336f8512584c5ad").into(),
|
||||
hex!("00000000000000000000000000000000000000000000000000000000000003e8").into(),
|
||||
hex!("0000000000000000000000000000000000000000000000000000000000000001").into(),
|
||||
],
|
||||
data: hex!("0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004b000f000000000000000100d184c103f7acc340847eee82a0b909e3358bc28d440edffa1352b13227e8ee646f3ea37456dec701345772617070656420457468657210574554481235003511000000000000000000000000000000000000000000").into(),
|
||||
},
|
||||
Proof {
|
||||
block_hash: hex!("05aaa60b0f27cce9e71909508527264b77ee14da7b5bf915fcc4e32715333213").into(),
|
||||
tx_index: 0,
|
||||
data: (vec![
|
||||
hex!("cf0d1c1ba57d1e0edfb59786c7e30c2b7e12bd54612b00cd21c4eaeecedf44fb").to_vec(),
|
||||
hex!("d21fc4f68ab05bc4dcb23c67008e92c4d466437cdd6ed7aad0c008944c185510").to_vec(),
|
||||
hex!("b9890f91ca0d77aa2a4adfaf9b9e40c94cac9e638b6d9797923865872944b646").to_vec(),
|
||||
], vec![
|
||||
hex!("f90131a0b601337b3aa10a671caa724eba641e759399979856141d3aea6b6b4ac59b889ba00c7d5dd48be9060221a02fb8fa213860b4c50d47046c8fa65ffaba5737d569e0a094601b62a1086cd9c9cb71a7ebff9e718f3217fd6e837efe4246733c0a196f63a06a4b0dd0aefc37b3c77828c8f07d1b7a2455ceb5dbfd3c77d7d6aeeddc2f7e8ca0d6e8e23142cdd8ec219e1f5d8b56aa18e456702b195deeaa210327284d42ade4a08a313d4c87023005d1ab631bbfe3f5de1e405d0e66d0bef3e033f1e5711b5521a0bf09a5d9a48b10ade82b8d6a5362a15921c8b5228a3487479b467db97411d82fa0f95cccae2a7c572ef3c566503e30bac2b2feb2d2f26eebf6d870dcf7f8cf59cea0d21fc4f68ab05bc4dcb23c67008e92c4d466437cdd6ed7aad0c008944c1855108080808080808080").to_vec(),
|
||||
hex!("f851a0b9890f91ca0d77aa2a4adfaf9b9e40c94cac9e638b6d9797923865872944b646a060a634b9280e3a23fb63375e7bbdd9ab07fd379ab6a67e2312bbc112195fa358808080808080808080808080808080").to_vec(),
|
||||
hex!("f9030820b9030402f90300018301d6e2b9010000000000000800000000000020040008000000000000000000000000400000008000000000000000000000000000000000000000000000000000000000042010000000001000000000000000000000000000000000040000000000000000000000000000000000000000000000008000000000000000002000000000000000000000000200000000000000200000000000100000000040000001000200008000000000000200000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000f901f5f87a942ffa5ecdbe006d30397c7636d3e015eee251369ff842a0c965575a00553e094ca7c5d14f02e107c258dda06867cbf9e0e69f80e71bbcc1a000000000000000000000000000000000000000000000000000000000000003e8a000000000000000000000000000000000000000000000000000000000000003e8f9011c94ee9170abfbf9421ad6dd07f6bdec9d89f2b581e0f863a01b11dcf133cc240f682dab2d3a8e4cd35c5da8c9cf99adac4336f8512584c5ada000000000000000000000000000000000000000000000000000000000000003e8a00000000000000000000000000000000000000000000000000000000000000001b8a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004b000f000000000000000100d184c103f7acc340847eee82a0b909e3358bc28d440edffa1352b13227e8ee646f3ea37456dec701345772617070656420457468657210574554481235003511000000000000000000000000000000000000000000f858948cf6147918a5cbb672703f879f385036f8793a24e1a01449abf21e49fd025f33495e77f7b1461caefdd3d4bb646424a3f445c4576a5ba0000000000000000000000000440edffa1352b13227e8ee646f3ea37456dec701").to_vec(),
|
||||
]),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_message_verification_header() -> CompactExecutionHeader {
|
||||
CompactExecutionHeader {
|
||||
parent_hash: hex!("04a7f6ab8282203562c62f38b0ab41d32aaebe2c7ea687702b463148a6429e04")
|
||||
.into(),
|
||||
block_number: 55,
|
||||
state_root: hex!("894d968712976d613519f973a317cb0781c7b039c89f27ea2b7ca193f7befdb3")
|
||||
.into(),
|
||||
receipts_root: hex!("cf0d1c1ba57d1e0edfb59786c7e30c2b7e12bd54612b00cd21c4eaeecedf44fb")
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "beacon-spec-mainnet")]
|
||||
pub mod mainnet {
|
||||
use super::*;
|
||||
|
||||
type Block = frame_system::mocking::MockBlock<Test>;
|
||||
use sp_runtime::BuildStorage;
|
||||
|
||||
frame_support::construct_runtime!(
|
||||
pub enum Test {
|
||||
System: frame_system::{Pallet, Call, Storage, Event<T>},
|
||||
Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent},
|
||||
EthereumBeaconClient: ethereum_beacon_client::{Pallet, Call, Storage, Event<T>},
|
||||
}
|
||||
);
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
pub const SS58Prefix: u8 = 42;
|
||||
}
|
||||
|
||||
impl frame_system::Config for Test {
|
||||
type BaseCallFilter = frame_support::traits::Everything;
|
||||
type OnSetCode = ();
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type RuntimeTask = RuntimeTask;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = u64;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = ();
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = SS58Prefix;
|
||||
type MaxConsumers = frame_support::traits::ConstU32<16>;
|
||||
type Nonce = u64;
|
||||
type Block = Block;
|
||||
}
|
||||
|
||||
impl pallet_timestamp::Config for Test {
|
||||
type Moment = u64;
|
||||
type OnTimestampSet = ();
|
||||
type MinimumPeriod = ();
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const ChainForkVersions: ForkVersions = ForkVersions{
|
||||
genesis: Fork {
|
||||
version: [0, 0, 16, 32], // 0x00001020
|
||||
epoch: 0,
|
||||
},
|
||||
altair: Fork {
|
||||
version: [1, 0, 16, 32], // 0x01001020
|
||||
epoch: 36660,
|
||||
},
|
||||
bellatrix: Fork {
|
||||
version: [2, 0, 16, 32], // 0x02001020
|
||||
epoch: 112260,
|
||||
},
|
||||
capella: Fork {
|
||||
version: [3, 0, 16, 32], // 0x03001020
|
||||
epoch: 162304,
|
||||
},
|
||||
};
|
||||
pub const ExecutionHeadersPruneThreshold: u32 = 10;
|
||||
}
|
||||
|
||||
impl ethereum_beacon_client::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type ForkVersions = ChainForkVersions;
|
||||
type MaxExecutionHeadersToKeep = ExecutionHeadersPruneThreshold;
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
// Build genesis storage according to the mock runtime.
|
||||
pub fn new_tester() -> sp_io::TestExternalities {
|
||||
let t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
let mut ext = sp_io::TestExternalities::new(t);
|
||||
let _ = ext.execute_with(|| Timestamp::set(RuntimeOrigin::signed(1), 30_000));
|
||||
ext
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
pub use crate::config::{
|
||||
SLOTS_PER_HISTORICAL_ROOT, SYNC_COMMITTEE_BITS_SIZE as SC_BITS_SIZE,
|
||||
SYNC_COMMITTEE_SIZE as SC_SIZE,
|
||||
};
|
||||
use frame_support::storage::types::OptionQuery;
|
||||
use snowbridge_core::RingBufferMapImpl;
|
||||
|
||||
// Specialize types based on configured sync committee size
|
||||
pub type SyncCommittee = primitives::SyncCommittee<SC_SIZE>;
|
||||
pub type SyncCommitteePrepared = primitives::SyncCommitteePrepared<SC_SIZE>;
|
||||
pub type SyncAggregate = primitives::SyncAggregate<SC_SIZE, SC_BITS_SIZE>;
|
||||
pub type CheckpointUpdate = primitives::CheckpointUpdate<SC_SIZE>;
|
||||
pub type Update = primitives::Update<SC_SIZE, SC_BITS_SIZE>;
|
||||
pub type NextSyncCommitteeUpdate = primitives::NextSyncCommitteeUpdate<SC_SIZE>;
|
||||
|
||||
pub use primitives::ExecutionHeaderUpdate;
|
||||
|
||||
/// ExecutionHeader ring buffer implementation
|
||||
pub type ExecutionHeaderBuffer<T> = RingBufferMapImpl<
|
||||
u32,
|
||||
<T as crate::Config>::MaxExecutionHeadersToKeep,
|
||||
crate::ExecutionHeaderIndex<T>,
|
||||
crate::ExecutionHeaderMapping<T>,
|
||||
crate::ExecutionHeaders<T>,
|
||||
OptionQuery,
|
||||
>;
|
||||
|
||||
/// FinalizedState ring buffer implementation
|
||||
pub(crate) type FinalizedBeaconStateBuffer<T> = RingBufferMapImpl<
|
||||
u32,
|
||||
crate::MaxFinalizedHeadersToKeep<T>,
|
||||
crate::FinalizedBeaconStateIndex<T>,
|
||||
crate::FinalizedBeaconStateMapping<T>,
|
||||
crate::FinalizedBeaconState<T>,
|
||||
OptionQuery,
|
||||
>;
|
||||
@@ -0,0 +1,68 @@
|
||||
//! Autogenerated weights for ethereum_beacon_client
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
|
||||
//! DATE: 2022-09-27, STEPS: `10`, REPEAT: 10, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("/tmp/snowbridge/spec.json"), DB CACHE: 1024
|
||||
|
||||
// Executed Command:
|
||||
// ./target/release/snowbridge
|
||||
// benchmark
|
||||
// pallet
|
||||
// --chain
|
||||
// /tmp/snowbridge/spec.json
|
||||
// --execution=wasm
|
||||
// --pallet
|
||||
// ethereum_beacon_client
|
||||
// --extrinsic
|
||||
// *
|
||||
// --steps
|
||||
// 10
|
||||
// --repeat
|
||||
// 10
|
||||
// --output
|
||||
// pallets/ethereum-beacon-client/src/weights.rs
|
||||
// --template
|
||||
// templates/module-weight-template.hbs
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
|
||||
use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use sp_std::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for ethereum_beacon_client.
|
||||
pub trait WeightInfo {
|
||||
fn force_checkpoint() -> Weight;
|
||||
fn submit() -> Weight;
|
||||
fn submit_with_sync_committee() -> Weight;
|
||||
fn submit_execution_header() -> Weight;
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests
|
||||
impl WeightInfo for () {
|
||||
fn force_checkpoint() -> Weight {
|
||||
Weight::from_parts(97_263_571_000_u64, 0)
|
||||
.saturating_add(Weight::from_parts(0, 3501))
|
||||
.saturating_add(RocksDbWeight::get().reads(2))
|
||||
.saturating_add(RocksDbWeight::get().writes(9))
|
||||
}
|
||||
fn submit() -> Weight {
|
||||
Weight::from_parts(26_051_019_000_u64, 0)
|
||||
.saturating_add(Weight::from_parts(0, 93857))
|
||||
.saturating_add(RocksDbWeight::get().reads(8))
|
||||
.saturating_add(RocksDbWeight::get().writes(4))
|
||||
}
|
||||
fn submit_with_sync_committee() -> Weight {
|
||||
Weight::from_parts(122_461_312_000_u64, 0)
|
||||
.saturating_add(Weight::from_parts(0, 93857))
|
||||
.saturating_add(RocksDbWeight::get().reads(6))
|
||||
.saturating_add(RocksDbWeight::get().writes(1))
|
||||
}
|
||||
fn submit_execution_header() -> Weight {
|
||||
Weight::from_parts(113_158_000_u64, 0)
|
||||
.saturating_add(Weight::from_parts(0, 3537))
|
||||
.saturating_add(RocksDbWeight::get().reads(5))
|
||||
.saturating_add(RocksDbWeight::get().writes(4))
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"header": {
|
||||
"slot": 3622,
|
||||
"proposer_index": 7,
|
||||
"parent_root": "0x254c9215f6cce83e21b9776afb482181639602d3cb58cf99452a6a4a4f603930",
|
||||
"state_root": "0xea98df6d30817d63f3e54ea118e2b1ba8675753c72dec1661c503d4eb43f9bdd",
|
||||
"body_root": "0x765a0616a31d38e0ca2d10f6e8b234dd3d07e16aa929bcbc4de775c93f1972fd"
|
||||
},
|
||||
"ancestry_proof": {
|
||||
"header_branch": [
|
||||
"0x7690506882ac8c5f01d00f3ade06439259a3a0261ef5d61ec44920678b4104e6",
|
||||
"0xf01aa0fdd7c9ef7b1affb7854fe8cbcc5c70643ee5b83e032faa702a0675a8cb",
|
||||
"0x273a7b300b75ffa2c765af50680aa836299264f2107f38010278822313181801",
|
||||
"0x30fe73a3bae6a31af32656ab759a4b67d27a213e01012b96cc4fedd0f2e77c75",
|
||||
"0x7246cb3a35f13a1f0bbf907887985bb5382c45f2aa1699dbca48a0a82d5330af",
|
||||
"0x5e7270e88a22dd4a905b2e76da2c8c358baeddd34de6c64a71bb1c80070ab717"
|
||||
],
|
||||
"finalized_block_root": "0xa6fdc5df11c1759d11c9f0353a666715e5677e9ffd7d414e44cff0970553f1c9"
|
||||
},
|
||||
"execution_header": {
|
||||
"parent_hash": "0x6c9657f1267ad6040ea017ff6d02b55c4ba25cb092b8326d321dd98d01d1ee64",
|
||||
"fee_recipient": "0x0000000000000000000000000000000000000000",
|
||||
"state_root": "0x01f975f7cdff9b0a8844304aa59062fe18af0fef4636539312dfe20d238600ba",
|
||||
"receipts_root": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
|
||||
"logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
"prev_randao": "0xcdfcab74bc26b3f4311afdc72d2d21d33a4b045187a01fa208a9d687a6d1d25c",
|
||||
"block_number": 3622,
|
||||
"gas_limit": 30000000,
|
||||
"gas_used": 0,
|
||||
"timestamp": 1685722543,
|
||||
"extra_data": "0xd983010b02846765746888676f312e31392e358664617277696e",
|
||||
"base_fee_per_gas": 7,
|
||||
"block_hash": "0x38c80e0e26cb80730df627d32f50266bd0fe32fb12b7606300ad81aa2b4033db",
|
||||
"transactions_root": "0x7ffe241ea60187fdb0187bfa22de35d1f9bed7ab061d9401fd47e34a54fbede1",
|
||||
"withdrawals_root": "0x28ba1834a3a7b657460ce79fa3a1d909ab8828fd557659d4d0554a9bdbc0ec30"
|
||||
},
|
||||
"execution_branch": [
|
||||
"0x005b8d55b34b4323bfd4773c28b09eb53bc87959e65411ccd23728c7e42d5ff2",
|
||||
"0x336488033fe5f3ef4ccc12af07b9370b92e553e35ecb4a337a1b1c0e4afe1e0e",
|
||||
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71",
|
||||
"0x7061330dada1ba1c602ba98f647a441885460ed0db00483fea1282385dfab84b"
|
||||
]
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"attested_header": {
|
||||
"slot": 3640,
|
||||
"proposer_index": 5,
|
||||
"parent_root": "0xf062fcec9c3379a08e6add37a834b1e39af395fc343973e44957ecebbf2ecddd",
|
||||
"state_root": "0xb1581cb62fe376e305e02f26463153f5dfb804d8df97ef40fc315c1bc30731ba",
|
||||
"body_root": "0x98461abcc6d130b7bcb9430292c8a269ea9f01082685347e2968d892f716067c"
|
||||
},
|
||||
"sync_aggregate": {
|
||||
"sync_committee_bits": "0xffffffff",
|
||||
"sync_committee_signature": "0x925c6e4b67890a7e28a7ca19853f88247e92014b9d233ac9058efd4f3827f0055db308debe17596e635b93727b5a851e1366ca801f30b03fdec722f45011504702a27646488b5ab5e3428fe7b4d4a50132f374612f66e45d68db27c568f96f08"
|
||||
},
|
||||
"signature_slot": 3641,
|
||||
"next_sync_committee_update": null,
|
||||
"finalized_header": {
|
||||
"slot": 3624,
|
||||
"proposer_index": 7,
|
||||
"parent_root": "0x7690506882ac8c5f01d00f3ade06439259a3a0261ef5d61ec44920678b4104e6",
|
||||
"state_root": "0x3726ebb8d9973977a71a8389caf5fc5830eeb8cd4fdfbbc7b0c4e6ca3e6a4090",
|
||||
"body_root": "0x0f9a3f0fa5a4ffaf7c10504c86f23e7d554366ffd069fe958a160b253c3fd409"
|
||||
},
|
||||
"finality_branch": [
|
||||
"0xc501000000000000000000000000000000000000000000000000000000000000",
|
||||
"0x10c726fac935bf9657cc7476d3cfa7bedec5983dcfb59e8a7df6d0a619e108d7",
|
||||
"0x83c3d5360d254f4a44be712c1f433e88e810b6d1e0e789e90bada9e36126b857",
|
||||
"0x97245fa01a89a6d7b4542cd731fef699f58b2bbaabdd6f641334c9e9eeae3a20",
|
||||
"0xc3d19c773f66ab94bc2106d5e75a3205398dd6e94b6f8a5716f347741eb9fc5a",
|
||||
"0x9e5040e56d765c1add56779a716be7497be27cba37f866cd8d34418d55e48715"
|
||||
],
|
||||
"block_roots_root": "0x29a54625749fa25f9e36df14a3baa335c58246bba2f8c7eb8b1ec2e4908e2fd0",
|
||||
"block_roots_branch": [
|
||||
"0x53616f9298818a8423c98adc47c92aaf82f0c5c911dc4ee5f88ba6d3022341c1",
|
||||
"0x5d2f1c4bce6f63f26cbe3fbf480281c04a6b14bea74350a88ee945354ecbd79d",
|
||||
"0x8333eefc7eaa4d10091e2014b3aae2bf6bd2d10c22c67100e189f8ab6caab261",
|
||||
"0x3edfa69130bc193dec47c27a5903f03d5262b75899b69c0e95ac1816a664a3e7",
|
||||
"0x5e046000f85aede8d4c28140b27778488d4ad21b1e16e345055d07ee53f2711b"
|
||||
]
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"header": {
|
||||
"slot": 3616,
|
||||
"proposer_index": 7,
|
||||
"parent_root": "0x6c5e8c7b32b7bfbb250fa8fd7bc348d7325fb2bfc869e4c506af6802fcad87f4",
|
||||
"state_root": "0x3e467e3429a1ae36572fe3fe1c953381242e950254cf97c7527a8cea8aa6c9de",
|
||||
"body_root": "0x7da749680d2b0b4f779047fcfe7d0c13d247f6d23478817fe9c6fbe07993adb2"
|
||||
},
|
||||
"current_sync_committee": {
|
||||
"pubkeys": [
|
||||
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
|
||||
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
|
||||
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
|
||||
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
|
||||
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
|
||||
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
|
||||
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
|
||||
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
|
||||
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
|
||||
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
|
||||
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
|
||||
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
|
||||
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
|
||||
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
|
||||
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
|
||||
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
|
||||
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
|
||||
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
|
||||
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
|
||||
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
|
||||
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
|
||||
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
|
||||
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
|
||||
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
|
||||
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
|
||||
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
|
||||
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
|
||||
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
|
||||
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"
|
||||
],
|
||||
"aggregate_pubkey": "0x8fe11476a05750c52618deb79918e2e674f56dfbf12dbce55ae4386d108e8a1e83c6326f5957e2ef19137582ce270dc6"
|
||||
},
|
||||
"current_sync_committee_branch": [
|
||||
"0x46af3f54acbea439b63aa5bb699c8f25ff584b23912366788f7c8e95011ce324",
|
||||
"0x41dcb71ec3b3940399118d28e09fdc58a8e33b818b8c5cbb933c59929504ca08",
|
||||
"0xfa53febb29348e3493a50c0e7c6d35796bf69c54dfc6f42f7600612789d0ed6d",
|
||||
"0x5e7ea1693066b604fc60d4657b43e7a4aafd3f4f54d9a740d2abe765e92d8385",
|
||||
"0x16c9bca64a82e80c23817bfec345d088e0adc3865e392965c1244f97979f816a"
|
||||
],
|
||||
"validators_root": "0x270d43e74ce340de4bca2b1936beca0f4f5408d9e78aec4850920baf659d5b69",
|
||||
"block_roots_root": "0x00f6bbdeac1e1a922a9bf0e78720c0bffe558d8195e8ede8cb72bbd295f242f2",
|
||||
"block_roots_branch": [
|
||||
"0x7a61086fb9e53ab4dd87243d6288c51793696168a73773277630da5b20bf6091",
|
||||
"0x60733905cdc5dd65d05161bb3138eecc47d6d6057ab36b0d36cf5a3200484143",
|
||||
"0x86d7de634ae45de5b3cbbc562dd976de7d06a3d96f83147413536e6b108c7a39",
|
||||
"0x0ada571c9e0da6fce8dd13e6d9ce173768521ac32e0af456634556176789fa6e",
|
||||
"0x2341538fd0aafbc1ff0f513545e5dcd4b8905dc9e00d6173480c18a4e8086ebc"
|
||||
]
|
||||
}
|
||||
Vendored
Executable
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"attested_header": {
|
||||
"slot": 3696,
|
||||
"proposer_index": 1,
|
||||
"parent_root": "0x04a63c5dfb726c31a32a72c1c426ff89e21363223d7096486b629f1d58abe5d8",
|
||||
"state_root": "0xbe20e69420cbf9400224ec5edeb0843776a2ccf945e9a3ba9311ae812cad1e30",
|
||||
"body_root": "0x1d2acd1748f1c58096d1edc8badd3a1d7e1dc3c33bcb9229e4c03f3a84efeadb"
|
||||
},
|
||||
"sync_aggregate": {
|
||||
"sync_committee_bits": "0xffffffff",
|
||||
"sync_committee_signature": "0xafa79bc0f3c731ab1eb6aeafc582a7dd1c100ea471df3af6ff485b58661b3ef8077264dea0b60df9aec2d3ca8ddab6770fc9d061462e5a6dc718146085425f863d00921c42413805cb5b4c5175f36f2087cfed740bb7d57e8d5b48352643cd5b"
|
||||
},
|
||||
"signature_slot": 3697,
|
||||
"next_sync_committee_update": null,
|
||||
"finalized_header": {
|
||||
"slot": 3680,
|
||||
"proposer_index": 7,
|
||||
"parent_root": "0x4d8f4fc47ad3eb045bd20cae13af6df02f96a3f8d7c8a285190ba10cfe2b84cf",
|
||||
"state_root": "0xd498766d77277fe16a6a4609ab3ac3a6e9887d162d8dfffdfc9cc4ae833e4127",
|
||||
"body_root": "0x9ba73bc9a4907cac0b887550e2b01a63dcc70473753ffcc243d33394cc64b4c0"
|
||||
},
|
||||
"finality_branch": [
|
||||
"0xcc01000000000000000000000000000000000000000000000000000000000000",
|
||||
"0x10c726fac935bf9657cc7476d3cfa7bedec5983dcfb59e8a7df6d0a619e108d7",
|
||||
"0x142061c4bc3673bf774cb8c7b6085057bd0ca85672b43afa2d9581b0b6a44e54",
|
||||
"0x48b8cd8ca9d9563e30c1cca2a854cd7f75eb4cb013d10809b3138a72d94ea0c5",
|
||||
"0x9b39523d05013ac7cbb9f43e5d6f9dc033b12aa1d6d6edd994ddc4f5efe7be9d",
|
||||
"0x066c9aa26107bc8cb28bc73e518da6cc865ec1d67516b6ca24663b6b7ae3cb21"
|
||||
],
|
||||
"block_roots_root": "0xb15aa2483811d8c5616cb93710f4fcb809d97443caac9de163f943a30f385db6",
|
||||
"block_roots_branch": [
|
||||
"0xf7a43ad317417daa4c2a1e93c54895895a824ef1e43320eb44eab16673da5a61",
|
||||
"0xe4b8d640660f765c2ef4dc886025dc8e54c6e70b66192582f42837ed5e9d8d41",
|
||||
"0x841f113dc81e76419b6cdec8b0cf2fc20f9381492ed3c79e9b49179b4d3eacbc",
|
||||
"0xeb5fdc4d8b5282b653ecbc9caa93bcfe482f6d6a32cbb0d9eb011bef947579bb",
|
||||
"0x1f328cc5640efb191ae6aa86223b1aa9d083b26ac3e1fa3c071327bb09dc5727"
|
||||
]
|
||||
}
|
||||
Vendored
Executable
+83
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"attested_header": {
|
||||
"slot": 3664,
|
||||
"proposer_index": 4,
|
||||
"parent_root": "0x15ac23a0c16bfa81e8595621118040c3e6cbddd4b09bae6fb39ba5fefd0258e8",
|
||||
"state_root": "0x6fb81aa3827e7d580bb05b4df2686c9a49508bde2f8342fd75be609a23dd8362",
|
||||
"body_root": "0x9906a1ae8065d268f8acb7f1b3119408d2f7f8e6e0764370c16ea3d15134981f"
|
||||
},
|
||||
"sync_aggregate": {
|
||||
"sync_committee_bits": "0xffffffff",
|
||||
"sync_committee_signature": "0xa9b5584ec9290a4ac6c5616639d031f9ab1064d63b4889f1da52f6f4d66b645fca48bbe2fe8484adb0c05c647edd694d0340cf684b8ccf8e34c6d8cf447cfcfdcb856f5abdcfd85ada5a4a04d4c8f6f40c6e99308893c3941485a436d6c8e5f7"
|
||||
},
|
||||
"signature_slot": 3665,
|
||||
"next_sync_committee_update": {
|
||||
"next_sync_committee": {
|
||||
"pubkeys": [
|
||||
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
|
||||
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
|
||||
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
|
||||
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
|
||||
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
|
||||
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
|
||||
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
|
||||
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
|
||||
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
|
||||
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
|
||||
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
|
||||
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
|
||||
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
|
||||
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
|
||||
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
|
||||
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
|
||||
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
|
||||
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
|
||||
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
|
||||
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
|
||||
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
|
||||
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
|
||||
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
|
||||
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
|
||||
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
|
||||
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
|
||||
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
|
||||
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373"
|
||||
],
|
||||
"aggregate_pubkey": "0x8fe11476a05750c52618deb79918e2e674f56dfbf12dbce55ae4386d108e8a1e83c6326f5957e2ef19137582ce270dc6"
|
||||
},
|
||||
"next_sync_committee_branch": [
|
||||
"0x46af3f54acbea439b63aa5bb699c8f25ff584b23912366788f7c8e95011ce324",
|
||||
"0x5b118fe110ee4a1b0cf9823bc189fb38eb55a7b49adbdafcf466ec7cd4b7fd68",
|
||||
"0xc2f12fb91a61abedb47f62a98258960edca21f31494cdf59b47a1c721e3e98f8",
|
||||
"0x16fdfd5e6b591b3140a76efa4593a9c4d105b9e5c62d6f44edbd24790657be50",
|
||||
"0xc8175ab66690cc94c0a24452754addd62a06948de5db9814e813437a130de452"
|
||||
]
|
||||
},
|
||||
"finalized_header": {
|
||||
"slot": 3648,
|
||||
"proposer_index": 1,
|
||||
"parent_root": "0x991ee98a70e8f90bdd61d0f5554e53d37473e75e16af171f6d88f27d20223dae",
|
||||
"state_root": "0x59b04d660ac772005a13a7dc1d5f99bb0d0292f3c422f04f7365198d70dd30de",
|
||||
"body_root": "0x5151f035e146258e7327ad9cf1df13f8ddec7a7842c19993cf739358717b5565"
|
||||
},
|
||||
"finality_branch": [
|
||||
"0xc801000000000000000000000000000000000000000000000000000000000000",
|
||||
"0x10c726fac935bf9657cc7476d3cfa7bedec5983dcfb59e8a7df6d0a619e108d7",
|
||||
"0x142061c4bc3673bf774cb8c7b6085057bd0ca85672b43afa2d9581b0b6a44e54",
|
||||
"0xc2f12fb91a61abedb47f62a98258960edca21f31494cdf59b47a1c721e3e98f8",
|
||||
"0x16fdfd5e6b591b3140a76efa4593a9c4d105b9e5c62d6f44edbd24790657be50",
|
||||
"0xc8175ab66690cc94c0a24452754addd62a06948de5db9814e813437a130de452"
|
||||
],
|
||||
"block_roots_root": "0xe6e2adaaad45363d7112945ef670e21c66bcb3276dc450962ade1e8950230380",
|
||||
"block_roots_branch": [
|
||||
"0x386ede102258966d4c23031c5a02de2af8180d475c4c1716b07fb5b9f142a817",
|
||||
"0x35e6c89bc38d993a1957f8a9fb1fbeab7420688091ba2cd7ee7b19b7e187f7d6",
|
||||
"0x99249309825cafef7e694c09c4fdf95eb4b1e8743d3b23f6959d9980ad2d69b0",
|
||||
"0x5e028d1d905db6430f0ce4aafbc78f442047ec3a132b4e69557fdf804a4cfbf3",
|
||||
"0xd34afeab37851937920243683a1c926c41c626aacb145718fce755782d4996dd"
|
||||
]
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"attested_header": {
|
||||
"slot": 3600,
|
||||
"proposer_index": 7,
|
||||
"parent_root": "0xdf60c2d58beccd89678b9267c689e9ba1cf1d58ce5114ad5c16e8341459cfd75",
|
||||
"state_root": "0x023f14c7a38ef4d6ec19b522edfb427c6b70c6ffbd8610ca802dd1491c92c852",
|
||||
"body_root": "0x0f78a1c45e42711efc5fb7b7f6238be1bee9273f7c44ff6892d815858bb77e25"
|
||||
},
|
||||
"sync_aggregate": {
|
||||
"sync_committee_bits": "0xffffffff",
|
||||
"sync_committee_signature": "0xa4dd8f0991de88ca6f81476f72f48cdb67b9414ad7bf6bba37f627c5ec84dd2c2ebc12cddd5d2e7c927276cee2d3d144158b4c067db3e9911fe52fe1875b14c93f90e4eb57bf5e8f0e6e6effe22f9ba076f30207e0ec683354961ae8e9779556"
|
||||
},
|
||||
"signature_slot": 3601,
|
||||
"next_sync_committee_update": {
|
||||
"next_sync_committee": {
|
||||
"pubkeys": [
|
||||
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
|
||||
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
|
||||
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
|
||||
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
|
||||
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
|
||||
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
|
||||
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
|
||||
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
|
||||
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
|
||||
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
|
||||
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
|
||||
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
|
||||
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
|
||||
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
|
||||
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
|
||||
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
|
||||
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
|
||||
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
|
||||
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
|
||||
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
|
||||
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
|
||||
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
|
||||
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
|
||||
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
|
||||
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
|
||||
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
|
||||
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
|
||||
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
|
||||
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"
|
||||
],
|
||||
"aggregate_pubkey": "0x8fe11476a05750c52618deb79918e2e674f56dfbf12dbce55ae4386d108e8a1e83c6326f5957e2ef19137582ce270dc6"
|
||||
},
|
||||
"next_sync_committee_branch": [
|
||||
"0x1446606d0129c324a4ea374bd29a625175e0659512cd8650097e0a9c38ce6379",
|
||||
"0xd92466c7e9a53b7b55f4fdb151746a3058931d7559b7e84e7b15384ddc903ca0",
|
||||
"0x9fd10c3f68b75cfd3ebd2af0d4e2cbbfbe120e0b5423dde89ff0f743c7a4f937",
|
||||
"0x1ed6aac0ab29a883de2bb2e3579ad4d6807ddcf3db8afcaf0ae25a076ac9a5f4",
|
||||
"0xf17a840df410a15f0e4e48abf521c29ad0d296d3fb4e8b847ea37f2cc8236f1f"
|
||||
]
|
||||
},
|
||||
"finalized_header": {
|
||||
"slot": 3584,
|
||||
"proposer_index": 1,
|
||||
"parent_root": "0x91c285af2ec25d485310391afe667108b787ec570cdbb0e3fd87b1e0e2c47bd7",
|
||||
"state_root": "0xccc4baf90024e035f1252520d2f2ef1e50f840ff0ecc8e6e365721e083871a32",
|
||||
"body_root": "0x91df5e0077434aad609aaa7e030005cee77cca83868ffc2724e5befe9a3f6a02"
|
||||
},
|
||||
"finality_branch": [
|
||||
"0xc001000000000000000000000000000000000000000000000000000000000000",
|
||||
"0x10c726fac935bf9657cc7476d3cfa7bedec5983dcfb59e8a7df6d0a619e108d7",
|
||||
"0x83c3d5360d254f4a44be712c1f433e88e810b6d1e0e789e90bada9e36126b857",
|
||||
"0x9fd10c3f68b75cfd3ebd2af0d4e2cbbfbe120e0b5423dde89ff0f743c7a4f937",
|
||||
"0x1ed6aac0ab29a883de2bb2e3579ad4d6807ddcf3db8afcaf0ae25a076ac9a5f4",
|
||||
"0xf17a840df410a15f0e4e48abf521c29ad0d296d3fb4e8b847ea37f2cc8236f1f"
|
||||
],
|
||||
"block_roots_root": "0x9eab8a05c396a29c32f4f8ac9654fc0fb7cd97ec659236392ede48951a794505",
|
||||
"block_roots_branch": [
|
||||
"0x5c175efdbafacdfdab21c93a318b0e8e2291a5a86c40b1fc564f91ad33c106d4",
|
||||
"0x5c1e0b76176ab033858b2835f90d5e25d708b563f77efd7d9938f0faa1c20878",
|
||||
"0x7aea32464adee801e2a05c3af227f24231d3c088e3b7265a5fada9ac850549fe",
|
||||
"0x9d9fca29e23c5d4ae433adf17e7fd9a0e4d1b09b68f5c45e7ca1b13ebe4a9e98",
|
||||
"0x6b35238f188021c859d6b317457ebb6fe4cf362cab35c988010cb1343eabbfc5"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
[package]
|
||||
name = "snowbridge-inbound-queue"
|
||||
description = "Snowbridge Inbound Queue"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
repository = "https://github.com/Snowfork/snowbridge"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.188", optional = true }
|
||||
codec = { version = "3.6.1", package = "parity-scale-codec", default-features = false, features = ["derive"] }
|
||||
scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
|
||||
hex-literal = { version = "0.4.1", optional = true }
|
||||
log = { version = "0.4.20", default-features = false }
|
||||
alloy-primitives = { version = "0.4.2", default-features = false, features = ["rlp"] }
|
||||
alloy-sol-types = { version = "0.4.2", default-features = false }
|
||||
alloy-rlp = { version = "0.3.3", default-features = false, features = ["derive"] }
|
||||
num-traits = { version = "0.2.16", default-features = false }
|
||||
|
||||
frame-benchmarking = { path = "../../../../../substrate/frame/benchmarking", default-features = false, optional = true }
|
||||
frame-support = { path = "../../../../../substrate/frame/support", default-features = false }
|
||||
frame-system = { path = "../../../../../substrate/frame/system", default-features = false }
|
||||
pallet-balances = { path = "../../../../../substrate/frame/balances", default-features = false }
|
||||
sp-core = { path = "../../../../../substrate/primitives/core", default-features = false }
|
||||
sp-std = { path = "../../../../../substrate/primitives/std", default-features = false }
|
||||
sp-io = { path = "../../../../../substrate/primitives/io", default-features = false }
|
||||
sp-runtime = { path = "../../../../../substrate/primitives/runtime", default-features = false }
|
||||
|
||||
xcm = { package = "staging-xcm", path = "../../../../../polkadot/xcm", default-features = false }
|
||||
xcm-builder = { package = "staging-xcm-builder", path = "../../../../../polkadot/xcm/xcm-builder", default-features = false }
|
||||
|
||||
snowbridge-core = { path = "../../primitives/core", default-features = false }
|
||||
snowbridge-ethereum = { path = "../../primitives/ethereum", default-features = false }
|
||||
snowbridge-router-primitives = { path = "../../primitives/router", default-features = false }
|
||||
snowbridge-beacon-primitives = { path = "../../primitives/beacon", default-features = false, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
frame-benchmarking = { path = "../../../../../substrate/frame/benchmarking" }
|
||||
sp-keyring = { path = "../../../../../substrate/primitives/keyring" }
|
||||
snowbridge-beacon-primitives = { path = "../../primitives/beacon" }
|
||||
snowbridge-ethereum-beacon-client = { path = "../../pallets/ethereum-beacon-client" }
|
||||
hex-literal = { version = "0.4.1" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"alloy-primitives/std",
|
||||
"alloy-rlp/std",
|
||||
"alloy-sol-types/std",
|
||||
"codec/std",
|
||||
"frame-benchmarking/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"log/std",
|
||||
"num-traits/std",
|
||||
"pallet-balances/std",
|
||||
"scale-info/std",
|
||||
"serde",
|
||||
"snowbridge-core/std",
|
||||
"snowbridge-ethereum/std",
|
||||
"snowbridge-router-primitives/std",
|
||||
"sp-core/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
"xcm-builder/std",
|
||||
"xcm/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"frame-benchmarking",
|
||||
"frame-benchmarking/runtime-benchmarks",
|
||||
"frame-support/runtime-benchmarks",
|
||||
"frame-system/runtime-benchmarks",
|
||||
"hex-literal",
|
||||
"pallet-balances/runtime-benchmarks",
|
||||
"snowbridge-beacon-primitives",
|
||||
"snowbridge-core/runtime-benchmarks",
|
||||
"snowbridge-ethereum-beacon-client/runtime-benchmarks",
|
||||
"snowbridge-router-primitives/runtime-benchmarks",
|
||||
"sp-runtime/runtime-benchmarks",
|
||||
"xcm-builder/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"frame-support/try-runtime",
|
||||
"frame-system/try-runtime",
|
||||
"pallet-balances/try-runtime",
|
||||
"snowbridge-ethereum-beacon-client/try-runtime",
|
||||
"sp-runtime/try-runtime",
|
||||
]
|
||||
@@ -0,0 +1,40 @@
|
||||
use hex_literal::hex;
|
||||
use snowbridge_beacon_primitives::CompactExecutionHeader;
|
||||
use snowbridge_core::inbound::{Log, Message, Proof};
|
||||
use sp_std::vec;
|
||||
|
||||
pub struct InboundQueueTest {
|
||||
pub execution_header: CompactExecutionHeader,
|
||||
pub message: Message,
|
||||
}
|
||||
|
||||
pub fn make_create_message() -> InboundQueueTest {
|
||||
InboundQueueTest{
|
||||
execution_header: CompactExecutionHeader{
|
||||
parent_hash: hex!("b5608f0af7c3b6fe5c593772fc25436b8d6549eb236adb0855c6ad33e0004e04").into(),
|
||||
block_number: 115,
|
||||
state_root: hex!("47ed174789836c622499d9659a4ac32c3b91a7b15642d39b0a11b82ff23995c1").into(),
|
||||
receipts_root: hex!("42c08b5303fcdf9e49c833fe5f1182cdbc8206bf8aec581125fc34aba11e1f1a").into(),
|
||||
},
|
||||
message: Message {
|
||||
event_log: Log {
|
||||
address: hex!("eda338e4dc46038493b885327842fd3e301cab39").into(),
|
||||
topics: vec![
|
||||
hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(),
|
||||
hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into(),
|
||||
hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0").into(),
|
||||
],
|
||||
data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002e00a736aa00000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d00e40b54020000000000000000000000000000000000000000000000000000000000").into(),
|
||||
},
|
||||
proof: Proof {
|
||||
block_hash: hex!("add15f439c8a57fe375d0a679870b1359921d70cb0e3e44f0dd3e272849f4097").into(),
|
||||
tx_index: 0,
|
||||
data: (vec![
|
||||
hex!("42c08b5303fcdf9e49c833fe5f1182cdbc8206bf8aec581125fc34aba11e1f1a").to_vec(),
|
||||
], vec![
|
||||
hex!("f9028e822080b9028802f90284018301ed20b9010000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000080000000000000000000000000000004000000000080000000000000000000000000000000000010100000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000040004000000000000002000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000200000000000010f90179f85894eda338e4dc46038493b885327842fd3e301cab39e1a0f78bb28d4b1d7da699e5c0bc2be29c2b04b5aab6aacf6298fe5304f9db9c6d7ea000000000000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7df9011c94eda338e4dc46038493b885327842fd3e301cab39f863a07153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84fa0c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539a05f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0b8a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002e00a736aa00000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d00e40b54020000000000000000000000000000000000000000000000000000000000").to_vec(),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
mod fixtures;
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use super::*;
|
||||
|
||||
use crate::Pallet as InboundQueue;
|
||||
use frame_benchmarking::v2::*;
|
||||
use frame_support::assert_ok;
|
||||
use frame_system::RawOrigin;
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
use crate::benchmarking::fixtures::make_create_message;
|
||||
|
||||
#[benchmark]
|
||||
fn submit() -> Result<(), BenchmarkError> {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
|
||||
let create_message = make_create_message();
|
||||
|
||||
T::Helper::initialize_storage(
|
||||
create_message.message.proof.block_hash,
|
||||
create_message.execution_header,
|
||||
);
|
||||
|
||||
let sovereign_account = sibling_sovereign_account::<T>(1000u32.into());
|
||||
|
||||
let minimum_balance = T::Token::minimum_balance();
|
||||
|
||||
// So that the receiving account exists
|
||||
assert_ok!(T::Token::mint_into(&caller, minimum_balance));
|
||||
// Fund the sovereign account (parachain sovereign account) so it can transfer a reward
|
||||
// fee to the caller account
|
||||
assert_ok!(T::Token::mint_into(
|
||||
&sovereign_account,
|
||||
3_000_000_000_000u128
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| panic!("unable to cast sovereign account balance")),
|
||||
));
|
||||
|
||||
#[block]
|
||||
{
|
||||
assert_ok!(InboundQueue::<T>::submit(
|
||||
RawOrigin::Signed(caller.clone()).into(),
|
||||
create_message.message,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(InboundQueue, crate::mock::new_tester(), crate::mock::Test);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use snowbridge_core::{inbound::Log, ChannelId};
|
||||
|
||||
use sp_core::{RuntimeDebug, H160, H256};
|
||||
use sp_std::{convert::TryFrom, prelude::*};
|
||||
|
||||
use alloy_primitives::B256;
|
||||
use alloy_sol_types::{sol, SolEvent};
|
||||
|
||||
sol! {
|
||||
event OutboundMessageAccepted(bytes32 indexed channel_id, uint64 nonce, bytes32 indexed message_id, bytes payload);
|
||||
}
|
||||
|
||||
/// An inbound message that has had its outer envelope decoded.
|
||||
#[derive(Clone, RuntimeDebug)]
|
||||
pub struct Envelope {
|
||||
/// The address of the outbound queue on Ethereum that emitted this message as an event log
|
||||
pub gateway: H160,
|
||||
/// The message Channel
|
||||
pub channel_id: ChannelId,
|
||||
/// A nonce for enforcing replay protection and ordering.
|
||||
pub nonce: u64,
|
||||
/// An id for tracing the message on its route (has no role in bridge consensus)
|
||||
pub message_id: H256,
|
||||
/// The inner payload generated from the source application.
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, RuntimeDebug)]
|
||||
pub struct EnvelopeDecodeError;
|
||||
|
||||
impl TryFrom<&Log> for Envelope {
|
||||
type Error = EnvelopeDecodeError;
|
||||
|
||||
fn try_from(log: &Log) -> Result<Self, Self::Error> {
|
||||
let topics: Vec<B256> = log.topics.iter().map(|x| B256::from_slice(x.as_ref())).collect();
|
||||
|
||||
let event = OutboundMessageAccepted::decode_log(topics, &log.data, true)
|
||||
.map_err(|_| EnvelopeDecodeError)?;
|
||||
|
||||
Ok(Self {
|
||||
gateway: log.address,
|
||||
channel_id: ChannelId::from(event.channel_id.as_ref()),
|
||||
nonce: event.nonce,
|
||||
message_id: H256::from(event.message_id.as_ref()),
|
||||
payload: event.payload,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Inbound Queue
|
||||
//!
|
||||
//! # Overview
|
||||
//!
|
||||
//! Receives messages emitted by the Gateway contract on Ethereum, whereupon they are verified,
|
||||
//! translated to XCM, and finally sent to their final destination parachain.
|
||||
//!
|
||||
//! The message relayers are rewarded using native currency from the sovereign account of the
|
||||
//! destination parachain.
|
||||
//!
|
||||
//! # Extrinsics
|
||||
//!
|
||||
//! ## Governance
|
||||
//!
|
||||
//! * [`Call::set_operating_mode`]: Set the operating mode of the pallet. Can be used to disable
|
||||
//! processing of inbound messages.
|
||||
//!
|
||||
//! ## Message Submission
|
||||
//!
|
||||
//! * [`Call::submit`]: Submit a message for verification and dispatch the final destination
|
||||
//! parachain.
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
mod envelope;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
use snowbridge_beacon_primitives::CompactExecutionHeader;
|
||||
|
||||
pub mod weights;
|
||||
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
use codec::{Decode, DecodeAll, Encode};
|
||||
use envelope::Envelope;
|
||||
use frame_support::{
|
||||
traits::{
|
||||
fungible::{Inspect, Mutate},
|
||||
tokens::{Fortitude, Precision, Preservation},
|
||||
},
|
||||
weights::WeightToFee,
|
||||
PalletError,
|
||||
};
|
||||
use frame_system::ensure_signed;
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::{H160, H256};
|
||||
use sp_std::{convert::TryFrom, vec};
|
||||
use xcm::prelude::{
|
||||
send_xcm, Instruction::SetTopic, Junction::*, Junctions::*, MultiLocation,
|
||||
SendError as XcmpSendError, SendXcm, Xcm, XcmHash,
|
||||
};
|
||||
|
||||
use snowbridge_core::{
|
||||
inbound::{Message, VerificationError, Verifier},
|
||||
sibling_sovereign_account, BasicOperatingMode, Channel, ChannelId, ParaId, StaticLookup,
|
||||
};
|
||||
use snowbridge_router_primitives::{
|
||||
inbound,
|
||||
inbound::{ConvertMessage, ConvertMessageError},
|
||||
};
|
||||
use sp_runtime::traits::Saturating;
|
||||
|
||||
pub use weights::WeightInfo;
|
||||
|
||||
type BalanceOf<T> =
|
||||
<<T as pallet::Config>::Token as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
pub const LOG_TARGET: &str = "snowbridge-inbound-queue";
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
|
||||
use frame_support::pallet_prelude::*;
|
||||
use frame_system::pallet_prelude::*;
|
||||
use snowbridge_core::PricingParameters;
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub trait BenchmarkHelper<T> {
|
||||
fn initialize_storage(block_hash: H256, header: CompactExecutionHeader);
|
||||
}
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config {
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||||
|
||||
/// The verifier for inbound messages from Ethereum
|
||||
type Verifier: Verifier;
|
||||
|
||||
/// Message relayers are rewarded with this asset
|
||||
type Token: Mutate<Self::AccountId> + Inspect<Self::AccountId>;
|
||||
|
||||
/// XCM message sender
|
||||
type XcmSender: SendXcm;
|
||||
|
||||
// Address of the Gateway contract
|
||||
#[pallet::constant]
|
||||
type GatewayAddress: Get<H160>;
|
||||
|
||||
/// Convert inbound message to XCM
|
||||
type MessageConverter: ConvertMessage<
|
||||
AccountId = Self::AccountId,
|
||||
Balance = BalanceOf<Self>,
|
||||
>;
|
||||
|
||||
/// Lookup a channel descriptor
|
||||
type ChannelLookup: StaticLookup<Source = ChannelId, Target = Channel>;
|
||||
|
||||
/// Lookup pricing parameters
|
||||
type PricingParameters: Get<PricingParameters<BalanceOf<Self>>>;
|
||||
|
||||
type WeightInfo: WeightInfo;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type Helper: BenchmarkHelper<Self>;
|
||||
|
||||
/// Convert a weight value into deductible balance type.
|
||||
type WeightToFee: WeightToFee<Balance = BalanceOf<Self>>;
|
||||
|
||||
/// Convert a length value into deductible balance type
|
||||
type LengthToFee: WeightToFee<Balance = BalanceOf<Self>>;
|
||||
|
||||
/// The upper limit here only used to estimate delivery cost
|
||||
type MaxMessageSize: Get<u32>;
|
||||
}
|
||||
|
||||
#[pallet::hooks]
|
||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T> {
|
||||
/// A message was received from Ethereum
|
||||
MessageReceived {
|
||||
/// The message channel
|
||||
channel_id: ChannelId,
|
||||
/// The message nonce
|
||||
nonce: u64,
|
||||
/// ID of the XCM message which was forwarded to the final destination parachain
|
||||
message_id: [u8; 32],
|
||||
},
|
||||
/// Set OperatingMode
|
||||
OperatingModeChanged { mode: BasicOperatingMode },
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// Message came from an invalid outbound channel on the Ethereum side.
|
||||
InvalidGateway,
|
||||
/// Message has an invalid envelope.
|
||||
InvalidEnvelope,
|
||||
/// Message has an unexpected nonce.
|
||||
InvalidNonce,
|
||||
/// Message has an invalid payload.
|
||||
InvalidPayload,
|
||||
/// Message channel is invalid
|
||||
InvalidChannel,
|
||||
/// The max nonce for the type has been reached
|
||||
MaxNonceReached,
|
||||
/// Cannot convert location
|
||||
InvalidAccountConversion,
|
||||
/// Pallet is halted
|
||||
Halted,
|
||||
/// Message verification error,
|
||||
Verification(VerificationError),
|
||||
/// XCMP send failure
|
||||
Send(SendError),
|
||||
/// Message conversion error
|
||||
ConvertMessage(ConvertMessageError),
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode, Eq, PartialEq, Debug, TypeInfo, PalletError)]
|
||||
pub enum SendError {
|
||||
NotApplicable,
|
||||
NotRoutable,
|
||||
Transport,
|
||||
DestinationUnsupported,
|
||||
ExceedsMaxMessageSize,
|
||||
MissingArgument,
|
||||
Fees,
|
||||
}
|
||||
|
||||
impl<T: Config> From<XcmpSendError> for Error<T> {
|
||||
fn from(e: XcmpSendError) -> Self {
|
||||
match e {
|
||||
XcmpSendError::NotApplicable => Error::<T>::Send(SendError::NotApplicable),
|
||||
XcmpSendError::Unroutable => Error::<T>::Send(SendError::NotRoutable),
|
||||
XcmpSendError::Transport(_) => Error::<T>::Send(SendError::Transport),
|
||||
XcmpSendError::DestinationUnsupported =>
|
||||
Error::<T>::Send(SendError::DestinationUnsupported),
|
||||
XcmpSendError::ExceedsMaxMessageSize =>
|
||||
Error::<T>::Send(SendError::ExceedsMaxMessageSize),
|
||||
XcmpSendError::MissingArgument => Error::<T>::Send(SendError::MissingArgument),
|
||||
XcmpSendError::Fees => Error::<T>::Send(SendError::Fees),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The current nonce for each channel
|
||||
#[pallet::storage]
|
||||
pub type Nonce<T: Config> = StorageMap<_, Twox64Concat, ChannelId, u64, ValueQuery>;
|
||||
|
||||
/// The current operating mode of the pallet.
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn operating_mode)]
|
||||
pub type OperatingMode<T: Config> = StorageValue<_, BasicOperatingMode, ValueQuery>;
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Submit an inbound message originating from the Gateway contract on Ethereum
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(T::WeightInfo::submit())]
|
||||
pub fn submit(origin: OriginFor<T>, message: Message) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
ensure!(!Self::operating_mode().is_halted(), Error::<T>::Halted);
|
||||
|
||||
// submit message to verifier for verification
|
||||
T::Verifier::verify(&message.event_log, &message.proof)
|
||||
.map_err(|e| Error::<T>::Verification(e))?;
|
||||
|
||||
// Decode event log into an Envelope
|
||||
let envelope =
|
||||
Envelope::try_from(&message.event_log).map_err(|_| Error::<T>::InvalidEnvelope)?;
|
||||
|
||||
// Verify that the message was submitted from the known Gateway contract
|
||||
ensure!(T::GatewayAddress::get() == envelope.gateway, Error::<T>::InvalidGateway);
|
||||
|
||||
// Retrieve the registered channel for this message
|
||||
let channel =
|
||||
T::ChannelLookup::lookup(envelope.channel_id).ok_or(Error::<T>::InvalidChannel)?;
|
||||
|
||||
// Verify message nonce
|
||||
<Nonce<T>>::try_mutate(envelope.channel_id, |nonce| -> DispatchResult {
|
||||
if *nonce == u64::MAX {
|
||||
return Err(Error::<T>::MaxNonceReached.into())
|
||||
}
|
||||
if envelope.nonce != nonce.saturating_add(1) {
|
||||
Err(Error::<T>::InvalidNonce.into())
|
||||
} else {
|
||||
*nonce = nonce.saturating_add(1);
|
||||
Ok(())
|
||||
}
|
||||
})?;
|
||||
|
||||
// Reward relayer from the sovereign account of the destination parachain
|
||||
// Expected to fail if sovereign account has no funds
|
||||
let sovereign_account = sibling_sovereign_account::<T>(channel.para_id);
|
||||
let delivery_cost = Self::calculate_delivery_cost(message.encode().len() as u32);
|
||||
T::Token::transfer(&sovereign_account, &who, delivery_cost, Preservation::Preserve)?;
|
||||
|
||||
// Decode message into XCM
|
||||
let (xcm, fee) =
|
||||
match inbound::VersionedMessage::decode_all(&mut envelope.payload.as_ref()) {
|
||||
Ok(message) => Self::do_convert(envelope.message_id, message)?,
|
||||
Err(_) => return Err(Error::<T>::InvalidPayload.into()),
|
||||
};
|
||||
|
||||
// We embed fees for xcm execution inside the xcm program using teleports
|
||||
// so we must burn the amount of the fee embedded into the XCM script.
|
||||
T::Token::burn_from(&sovereign_account, fee, Precision::Exact, Fortitude::Polite)?;
|
||||
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"💫 xcm {:?} sent with fee {:?}",
|
||||
xcm,
|
||||
fee
|
||||
);
|
||||
|
||||
// Attempt to send XCM to a dest parachain
|
||||
let message_id = Self::send_xcm(xcm, channel.para_id)?;
|
||||
|
||||
Self::deposit_event(Event::MessageReceived {
|
||||
channel_id: envelope.channel_id,
|
||||
nonce: envelope.nonce,
|
||||
message_id,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Halt or resume all pallet operations. May only be called by root.
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
|
||||
pub fn set_operating_mode(
|
||||
origin: OriginFor<T>,
|
||||
mode: BasicOperatingMode,
|
||||
) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
OperatingMode::<T>::set(mode);
|
||||
Self::deposit_event(Event::OperatingModeChanged { mode });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
pub fn do_convert(
|
||||
message_id: H256,
|
||||
message: inbound::VersionedMessage,
|
||||
) -> Result<(Xcm<()>, BalanceOf<T>), Error<T>> {
|
||||
let (mut xcm, fee) =
|
||||
T::MessageConverter::convert(message).map_err(|e| Error::<T>::ConvertMessage(e))?;
|
||||
// Append the message id as an XCM topic
|
||||
xcm.inner_mut().extend(vec![SetTopic(message_id.into())]);
|
||||
Ok((xcm, fee))
|
||||
}
|
||||
|
||||
pub fn send_xcm(xcm: Xcm<()>, dest: ParaId) -> Result<XcmHash, Error<T>> {
|
||||
let dest = MultiLocation { parents: 1, interior: X1(Parachain(dest.into())) };
|
||||
let (xcm_hash, _) = send_xcm::<T::XcmSender>(dest, xcm).map_err(Error::<T>::from)?;
|
||||
Ok(xcm_hash)
|
||||
}
|
||||
|
||||
pub fn calculate_delivery_cost(length: u32) -> BalanceOf<T> {
|
||||
let weight_fee = T::WeightToFee::weight_to_fee(&T::WeightInfo::submit());
|
||||
let len_fee = T::LengthToFee::weight_to_fee(&Weight::from_parts(length as u64, 0));
|
||||
weight_fee
|
||||
.saturating_add(len_fee)
|
||||
.saturating_add(T::PricingParameters::get().rewards.local)
|
||||
}
|
||||
}
|
||||
|
||||
/// API for accessing the delivery cost of a message
|
||||
impl<T: Config> Get<BalanceOf<T>> for Pallet<T> {
|
||||
fn get() -> BalanceOf<T> {
|
||||
// Cost here based on MaxMessagePayloadSize(the worst case)
|
||||
Self::calculate_delivery_cost(T::MaxMessageSize::get())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use super::*;
|
||||
|
||||
use frame_support::{
|
||||
parameter_types,
|
||||
traits::{ConstU128, ConstU32, Everything},
|
||||
weights::IdentityFee,
|
||||
};
|
||||
use hex_literal::hex;
|
||||
use snowbridge_beacon_primitives::{Fork, ForkVersions};
|
||||
use snowbridge_core::{
|
||||
gwei,
|
||||
inbound::{Log, Proof, VerificationError},
|
||||
meth, Channel, ChannelId, PricingParameters, Rewards, StaticLookup,
|
||||
};
|
||||
use snowbridge_router_primitives::inbound::MessageToXcm;
|
||||
use sp_core::{H160, H256};
|
||||
use sp_runtime::{
|
||||
traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify},
|
||||
BuildStorage, FixedU128, MultiSignature,
|
||||
};
|
||||
use sp_std::convert::From;
|
||||
use xcm::v3::{prelude::*, MultiAssets, SendXcm};
|
||||
|
||||
use crate::{self as inbound_queue};
|
||||
|
||||
type Block = frame_system::mocking::MockBlock<Test>;
|
||||
|
||||
frame_support::construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: frame_system::{Pallet, Call, Storage, Event<T>},
|
||||
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
|
||||
EthereumBeaconClient: snowbridge_ethereum_beacon_client::{Pallet, Call, Storage, Event<T>},
|
||||
InboundQueue: inbound_queue::{Pallet, Call, Storage, Event<T>},
|
||||
}
|
||||
);
|
||||
|
||||
pub type Signature = MultiSignature;
|
||||
pub type AccountId = <<Signature as Verify>::Signer as IdentifyAccount>::AccountId;
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
}
|
||||
|
||||
type Balance = u128;
|
||||
|
||||
impl frame_system::Config for Test {
|
||||
type BaseCallFilter = Everything;
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type RuntimeTask = RuntimeTask;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type DbWeight = ();
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = pallet_balances::AccountData<u128>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = frame_support::traits::ConstU32<16>;
|
||||
type Nonce = u64;
|
||||
type Block = Block;
|
||||
}
|
||||
|
||||
impl pallet_balances::Config for Test {
|
||||
type MaxLocks = ();
|
||||
type MaxReserves = ();
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
type Balance = Balance;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type DustRemoval = ();
|
||||
type ExistentialDeposit = ConstU128<1>;
|
||||
type AccountStore = System;
|
||||
type WeightInfo = ();
|
||||
type FreezeIdentifier = ();
|
||||
type MaxFreezes = ();
|
||||
type RuntimeHoldReason = ();
|
||||
type RuntimeFreezeReason = ();
|
||||
type MaxHolds = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const ExecutionHeadersPruneThreshold: u32 = 10;
|
||||
pub const ChainForkVersions: ForkVersions = ForkVersions{
|
||||
genesis: Fork {
|
||||
version: [0, 0, 0, 1], // 0x00000001
|
||||
epoch: 0,
|
||||
},
|
||||
altair: Fork {
|
||||
version: [1, 0, 0, 1], // 0x01000001
|
||||
epoch: 0,
|
||||
},
|
||||
bellatrix: Fork {
|
||||
version: [2, 0, 0, 1], // 0x02000001
|
||||
epoch: 0,
|
||||
},
|
||||
capella: Fork {
|
||||
version: [3, 0, 0, 1], // 0x03000001
|
||||
epoch: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
impl snowbridge_ethereum_beacon_client::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type ForkVersions = ChainForkVersions;
|
||||
type MaxExecutionHeadersToKeep = ExecutionHeadersPruneThreshold;
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
// Mock verifier
|
||||
pub struct MockVerifier;
|
||||
|
||||
impl Verifier for MockVerifier {
|
||||
fn verify(_: &Log, _: &Proof) -> Result<(), VerificationError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const GATEWAY_ADDRESS: [u8; 20] = hex!["eda338e4dc46038493b885327842fd3e301cab39"];
|
||||
|
||||
parameter_types! {
|
||||
pub const EthereumNetwork: xcm::v3::NetworkId = xcm::v3::NetworkId::Ethereum { chain_id: 11155111 };
|
||||
pub const GatewayAddress: H160 = H160(GATEWAY_ADDRESS);
|
||||
pub const CreateAssetCall: [u8;2] = [53, 0];
|
||||
pub const CreateAssetExecutionFee: u128 = 2_000_000_000;
|
||||
pub const CreateAssetDeposit: u128 = 100_000_000_000;
|
||||
pub const SendTokenExecutionFee: u128 = 1_000_000_000;
|
||||
pub const InitialFund: u128 = 1_000_000_000_000;
|
||||
pub const InboundQueuePalletInstance: u8 = 80;
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
impl<T: snowbridge_ethereum_beacon_client::Config> BenchmarkHelper<T> for Test {
|
||||
// not implemented since the MockVerifier is used for tests
|
||||
fn initialize_storage(_: H256, _: CompactExecutionHeader) {}
|
||||
}
|
||||
|
||||
// Mock XCM sender that always succeeds
|
||||
pub struct MockXcmSender;
|
||||
|
||||
impl SendXcm for MockXcmSender {
|
||||
type Ticket = Xcm<()>;
|
||||
|
||||
fn validate(
|
||||
dest: &mut Option<MultiLocation>,
|
||||
xcm: &mut Option<xcm::v3::Xcm<()>>,
|
||||
) -> SendResult<Self::Ticket> {
|
||||
match dest {
|
||||
Some(MultiLocation { interior, .. }) => {
|
||||
if let X1(Parachain(1001)) = interior {
|
||||
return Err(XcmpSendError::NotApplicable)
|
||||
}
|
||||
Ok((xcm.clone().unwrap(), MultiAssets::default()))
|
||||
},
|
||||
_ => Ok((xcm.clone().unwrap(), MultiAssets::default())),
|
||||
}
|
||||
}
|
||||
|
||||
fn deliver(xcm: Self::Ticket) -> core::result::Result<XcmHash, XcmpSendError> {
|
||||
let hash = xcm.using_encoded(sp_io::hashing::blake2_256);
|
||||
Ok(hash)
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const OwnParaId: ParaId = ParaId::new(1013);
|
||||
pub Parameters: PricingParameters<u128> = PricingParameters {
|
||||
exchange_rate: FixedU128::from_rational(1, 400),
|
||||
fee_per_gas: gwei(20),
|
||||
rewards: Rewards { local: DOT, remote: meth(1) }
|
||||
};
|
||||
}
|
||||
|
||||
pub const DOT: u128 = 10_000_000_000;
|
||||
|
||||
pub struct MockChannelLookup;
|
||||
impl StaticLookup for MockChannelLookup {
|
||||
type Source = ChannelId;
|
||||
type Target = Channel;
|
||||
|
||||
fn lookup(channel_id: Self::Source) -> Option<Self::Target> {
|
||||
if channel_id !=
|
||||
hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into()
|
||||
{
|
||||
return None
|
||||
}
|
||||
Some(Channel { agent_id: H256::zero(), para_id: ASSET_HUB_PARAID.into() })
|
||||
}
|
||||
}
|
||||
|
||||
impl inbound_queue::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Verifier = MockVerifier;
|
||||
type Token = Balances;
|
||||
type XcmSender = MockXcmSender;
|
||||
type WeightInfo = ();
|
||||
type GatewayAddress = GatewayAddress;
|
||||
type MessageConverter = MessageToXcm<
|
||||
CreateAssetCall,
|
||||
CreateAssetDeposit,
|
||||
InboundQueuePalletInstance,
|
||||
AccountId,
|
||||
Balance,
|
||||
>;
|
||||
type PricingParameters = Parameters;
|
||||
type ChannelLookup = MockChannelLookup;
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type Helper = Test;
|
||||
type WeightToFee = IdentityFee<u128>;
|
||||
type LengthToFee = IdentityFee<u128>;
|
||||
type MaxMessageSize = ConstU32<1024>;
|
||||
}
|
||||
|
||||
pub fn last_events(n: usize) -> Vec<RuntimeEvent> {
|
||||
frame_system::Pallet::<Test>::events()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.take(n)
|
||||
.rev()
|
||||
.map(|e| e.event)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn expect_events(e: Vec<RuntimeEvent>) {
|
||||
assert_eq!(last_events(e.len()), e);
|
||||
}
|
||||
|
||||
pub fn setup() {
|
||||
System::set_block_number(1);
|
||||
Balances::mint_into(
|
||||
&sibling_sovereign_account::<Test>(ASSET_HUB_PARAID.into()),
|
||||
InitialFund::get(),
|
||||
)
|
||||
.unwrap();
|
||||
Balances::mint_into(
|
||||
&sibling_sovereign_account::<Test>(TEMPLATE_PARAID.into()),
|
||||
InitialFund::get(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn new_tester() -> sp_io::TestExternalities {
|
||||
let storage = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
let mut ext: sp_io::TestExternalities = storage.into();
|
||||
ext.execute_with(setup);
|
||||
ext
|
||||
}
|
||||
|
||||
// Generated from smoketests:
|
||||
// cd smoketests
|
||||
// ./make-bindings
|
||||
// cargo test --test register_token -- --nocapture
|
||||
pub fn mock_event_log() -> Log {
|
||||
Log {
|
||||
// gateway address
|
||||
address: hex!("eda338e4dc46038493b885327842fd3e301cab39").into(),
|
||||
topics: vec![
|
||||
hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(),
|
||||
// channel id
|
||||
hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into(),
|
||||
// message id
|
||||
hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0").into(),
|
||||
],
|
||||
// Nonce + Payload
|
||||
data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002e000f000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d00e40b54020000000000000000000000000000000000000000000000000000000000").into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mock_event_log_invalid_channel() -> Log {
|
||||
Log {
|
||||
address: hex!("eda338e4dc46038493b885327842fd3e301cab39").into(),
|
||||
topics: vec![
|
||||
hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(),
|
||||
// invalid channel id
|
||||
hex!("0000000000000000000000000000000000000000000000000000000000000000").into(),
|
||||
hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0").into(),
|
||||
],
|
||||
data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001e000f000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d0000").into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mock_event_log_invalid_gateway() -> Log {
|
||||
Log {
|
||||
// gateway address
|
||||
address: H160::zero(),
|
||||
topics: vec![
|
||||
hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(),
|
||||
// channel id
|
||||
hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into(),
|
||||
// message id
|
||||
hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0").into(),
|
||||
],
|
||||
// Nonce + Payload
|
||||
data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001e000f000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d0000").into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const ASSET_HUB_PARAID: u32 = 1000u32;
|
||||
pub const TEMPLATE_PARAID: u32 = 1001u32;
|
||||
@@ -0,0 +1,211 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use super::*;
|
||||
|
||||
use frame_support::{assert_noop, assert_ok};
|
||||
use hex_literal::hex;
|
||||
use snowbridge_core::{inbound::Proof, ChannelId};
|
||||
use sp_keyring::AccountKeyring as Keyring;
|
||||
use sp_runtime::{DispatchError, TokenError};
|
||||
use sp_std::convert::From;
|
||||
|
||||
use crate::{Error, Event as InboundQueueEvent};
|
||||
|
||||
use crate::mock::*;
|
||||
|
||||
#[test]
|
||||
fn test_submit_happy_path() {
|
||||
new_tester().execute_with(|| {
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
let channel_sovereign = sibling_sovereign_account::<Test>(ASSET_HUB_PARAID.into());
|
||||
|
||||
let origin = RuntimeOrigin::signed(relayer.clone());
|
||||
|
||||
// Submit message
|
||||
let message = Message {
|
||||
event_log: mock_event_log(),
|
||||
proof: Proof {
|
||||
block_hash: Default::default(),
|
||||
tx_index: Default::default(),
|
||||
data: Default::default(),
|
||||
},
|
||||
};
|
||||
|
||||
let initial_fund = InitialFund::get();
|
||||
assert_eq!(Balances::balance(&relayer), 0);
|
||||
assert_eq!(Balances::balance(&channel_sovereign), initial_fund);
|
||||
|
||||
assert_ok!(InboundQueue::submit(origin.clone(), message.clone()));
|
||||
expect_events(vec![InboundQueueEvent::MessageReceived {
|
||||
channel_id: hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539")
|
||||
.into(),
|
||||
nonce: 1,
|
||||
message_id: [
|
||||
27, 217, 88, 127, 46, 143, 199, 70, 236, 66, 212, 244, 85, 221, 153, 104, 175, 37,
|
||||
224, 20, 140, 95, 140, 7, 27, 74, 182, 199, 77, 12, 194, 236,
|
||||
],
|
||||
}
|
||||
.into()]);
|
||||
|
||||
let delivery_cost = InboundQueue::calculate_delivery_cost(message.encode().len() as u32);
|
||||
assert!(
|
||||
Parameters::get().rewards.local < delivery_cost,
|
||||
"delivery cost exceeds pure reward"
|
||||
);
|
||||
|
||||
assert_eq!(Balances::balance(&relayer), delivery_cost, "relayer was rewarded");
|
||||
assert!(
|
||||
Balances::balance(&channel_sovereign) <= initial_fund - delivery_cost,
|
||||
"sovereign account paid reward"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_submit_xcm_invalid_channel() {
|
||||
new_tester().execute_with(|| {
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
let origin = RuntimeOrigin::signed(relayer);
|
||||
|
||||
// Deposit funds into sovereign account of parachain 1001
|
||||
let sovereign_account = sibling_sovereign_account::<Test>(TEMPLATE_PARAID.into());
|
||||
println!("account: {}", sovereign_account);
|
||||
let _ = Balances::mint_into(&sovereign_account, 10000);
|
||||
|
||||
// Submit message
|
||||
let message = Message {
|
||||
event_log: mock_event_log_invalid_channel(),
|
||||
proof: Proof {
|
||||
block_hash: Default::default(),
|
||||
tx_index: Default::default(),
|
||||
data: Default::default(),
|
||||
},
|
||||
};
|
||||
assert_noop!(
|
||||
InboundQueue::submit(origin.clone(), message.clone()),
|
||||
Error::<Test>::InvalidChannel,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_submit_with_invalid_gateway() {
|
||||
new_tester().execute_with(|| {
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
let origin = RuntimeOrigin::signed(relayer);
|
||||
|
||||
// Deposit funds into sovereign account of Asset Hub (Statemint)
|
||||
let sovereign_account = sibling_sovereign_account::<Test>(ASSET_HUB_PARAID.into());
|
||||
let _ = Balances::mint_into(&sovereign_account, 10000);
|
||||
|
||||
// Submit message
|
||||
let message = Message {
|
||||
event_log: mock_event_log_invalid_gateway(),
|
||||
proof: Proof {
|
||||
block_hash: Default::default(),
|
||||
tx_index: Default::default(),
|
||||
data: Default::default(),
|
||||
},
|
||||
};
|
||||
assert_noop!(
|
||||
InboundQueue::submit(origin.clone(), message.clone()),
|
||||
Error::<Test>::InvalidGateway
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_submit_with_invalid_nonce() {
|
||||
new_tester().execute_with(|| {
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
let origin = RuntimeOrigin::signed(relayer);
|
||||
|
||||
// Deposit funds into sovereign account of Asset Hub (Statemint)
|
||||
let sovereign_account = sibling_sovereign_account::<Test>(ASSET_HUB_PARAID.into());
|
||||
let _ = Balances::mint_into(&sovereign_account, 10000);
|
||||
|
||||
// Submit message
|
||||
let message = Message {
|
||||
event_log: mock_event_log(),
|
||||
proof: Proof {
|
||||
block_hash: Default::default(),
|
||||
tx_index: Default::default(),
|
||||
data: Default::default(),
|
||||
},
|
||||
};
|
||||
assert_ok!(InboundQueue::submit(origin.clone(), message.clone()));
|
||||
|
||||
let nonce: u64 = <Nonce<Test>>::get(ChannelId::from(hex!(
|
||||
"c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539"
|
||||
)));
|
||||
assert_eq!(nonce, 1);
|
||||
|
||||
// Submit the same again
|
||||
assert_noop!(
|
||||
InboundQueue::submit(origin.clone(), message.clone()),
|
||||
Error::<Test>::InvalidNonce
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_submit_no_funds_to_reward_relayers() {
|
||||
new_tester().execute_with(|| {
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
let origin = RuntimeOrigin::signed(relayer);
|
||||
|
||||
// Reset balance of sovereign_account to zero so to trigger the FundsUnavailable error
|
||||
let sovereign_account = sibling_sovereign_account::<Test>(ASSET_HUB_PARAID.into());
|
||||
Balances::set_balance(&sovereign_account, 0);
|
||||
|
||||
// Submit message
|
||||
let message = Message {
|
||||
event_log: mock_event_log(),
|
||||
proof: Proof {
|
||||
block_hash: Default::default(),
|
||||
tx_index: Default::default(),
|
||||
data: Default::default(),
|
||||
},
|
||||
};
|
||||
assert_noop!(
|
||||
InboundQueue::submit(origin.clone(), message.clone()),
|
||||
TokenError::FundsUnavailable
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_operating_mode() {
|
||||
new_tester().execute_with(|| {
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
let origin = RuntimeOrigin::signed(relayer);
|
||||
let message = Message {
|
||||
event_log: mock_event_log(),
|
||||
proof: Proof {
|
||||
block_hash: Default::default(),
|
||||
tx_index: Default::default(),
|
||||
data: Default::default(),
|
||||
},
|
||||
};
|
||||
|
||||
assert_ok!(InboundQueue::set_operating_mode(
|
||||
RuntimeOrigin::root(),
|
||||
snowbridge_core::BasicOperatingMode::Halted
|
||||
));
|
||||
|
||||
assert_noop!(InboundQueue::submit(origin, message), Error::<Test>::Halted);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_operating_mode_root_only() {
|
||||
new_tester().execute_with(|| {
|
||||
assert_noop!(
|
||||
InboundQueue::set_operating_mode(
|
||||
RuntimeOrigin::signed(Keyring::Bob.into()),
|
||||
snowbridge_core::BasicOperatingMode::Halted
|
||||
),
|
||||
DispatchError::BadOrigin
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Autogenerated weights for `snowbridge_inbound_queue`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
|
||||
//! DATE: 2023-07-14, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `macbook pro 14 m2`, CPU: `m2-arm64`
|
||||
//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("bridge-hub-rococo-dev"), DB CACHE: 1024
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
|
||||
use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use sp_std::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for ethereum_beacon_client.
|
||||
pub trait WeightInfo {
|
||||
fn submit() -> Weight;
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests
|
||||
impl WeightInfo for () {
|
||||
fn submit() -> Weight {
|
||||
Weight::from_parts(70_000_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 3601))
|
||||
.saturating_add(RocksDbWeight::get().reads(2))
|
||||
.saturating_add(RocksDbWeight::get().writes(2))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
[package]
|
||||
name = "snowbridge-outbound-queue"
|
||||
description = "Snowbridge Outbound Queue"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
repository = "https://github.com/Snowfork/snowbridge"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.188", features = ["alloc", "derive"], default-features = false }
|
||||
codec = { version = "3.6.1", package = "parity-scale-codec", default-features = false, features = ["derive"] }
|
||||
scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
|
||||
hex-literal = { version = "0.4.1", optional = true }
|
||||
|
||||
frame-benchmarking = { path = "../../../../../substrate/frame/benchmarking", default-features = false, optional = true }
|
||||
frame-support = { path = "../../../../../substrate/frame/support", default-features = false }
|
||||
frame-system = { path = "../../../../../substrate/frame/system", default-features = false }
|
||||
sp-core = { path = "../../../../../substrate/primitives/core", default-features = false }
|
||||
sp-std = { path = "../../../../../substrate/primitives/std", default-features = false }
|
||||
sp-runtime = { path = "../../../../../substrate/primitives/runtime", default-features = false }
|
||||
sp-io = { path = "../../../../../substrate/primitives/io", default-features = false }
|
||||
sp-arithmetic = { path = "../../../../../substrate/primitives/arithmetic", default-features = false }
|
||||
|
||||
bridge-hub-common = { path = "../../../../../cumulus/parachains/runtimes/bridge-hubs/common", default-features = false }
|
||||
|
||||
snowbridge-core = { path = "../../primitives/core", features = ["serde"], default-features = false }
|
||||
snowbridge-outbound-queue-merkle-tree = { path = "merkle-tree", default-features = false }
|
||||
ethabi = { git = "https://github.com/snowfork/ethabi-decode.git", package = "ethabi-decode", branch = "master", default-features = false }
|
||||
|
||||
xcm = { package = "staging-xcm", path = "../../../../../polkadot/xcm", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
pallet-message-queue = { path = "../../../../../substrate/frame/message-queue", default-features = false }
|
||||
sp-keyring = { path = "../../../../../substrate/primitives/keyring" }
|
||||
hex-literal = { version = "0.4.1" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"bridge-hub-common/std",
|
||||
"codec/std",
|
||||
"ethabi/std",
|
||||
"frame-benchmarking/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"pallet-message-queue/std",
|
||||
"scale-info/std",
|
||||
"serde/std",
|
||||
"snowbridge-core/std",
|
||||
"snowbridge-outbound-queue-merkle-tree/std",
|
||||
"sp-arithmetic/std",
|
||||
"sp-core/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
"xcm/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"bridge-hub-common/runtime-benchmarks",
|
||||
"frame-benchmarking",
|
||||
"frame-benchmarking/runtime-benchmarks",
|
||||
"frame-support/runtime-benchmarks",
|
||||
"frame-system/runtime-benchmarks",
|
||||
"hex-literal",
|
||||
"pallet-message-queue/runtime-benchmarks",
|
||||
"snowbridge-core/runtime-benchmarks",
|
||||
"sp-runtime/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"frame-support/try-runtime",
|
||||
"frame-system/try-runtime",
|
||||
"pallet-message-queue/try-runtime",
|
||||
"sp-runtime/try-runtime",
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "snowbridge-outbound-queue-merkle-tree"
|
||||
description = "Snowbridge Outbound Queue Merkle Tree"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
repository = "https://github.com/Snowfork/snowbridge"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { version = "3.1.5", package = "parity-scale-codec", default-features = false, features = ["derive"] }
|
||||
scale-info = { version = "2.7.0", default-features = false, features = ["derive"] }
|
||||
|
||||
sp-core = { path = "../../../../../../substrate/primitives/core", default-features = false }
|
||||
sp-runtime = { path = "../../../../../../substrate/primitives/runtime", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = { version = "0.4.1" }
|
||||
env_logger = "0.9"
|
||||
hex = "0.4"
|
||||
array-bytes = "4.1"
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"scale-info/std",
|
||||
"sp-core/std",
|
||||
"sp-runtime/std",
|
||||
]
|
||||
@@ -0,0 +1,464 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
// SPDX-FileCopyrightText: 2021-2022 Parity Technologies (UK) Ltd.
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
//! This crate implements a simple binary Merkle Tree utilities required for inter-op with Ethereum
|
||||
//! bridge & Solidity contract.
|
||||
//!
|
||||
//! The implementation is optimised for usage within Substrate Runtime and supports no-std
|
||||
//! compilation targets.
|
||||
//!
|
||||
//! Merkle Tree is constructed from arbitrary-length leaves, that are initially hashed using the
|
||||
//! same `\[`Hasher`\]` as the inner nodes.
|
||||
//! Inner nodes are created by concatenating child hashes and hashing again. The implementation
|
||||
//! does not perform any sorting of the input data (leaves) nor when inner nodes are created.
|
||||
//!
|
||||
//! If the number of leaves is not even, last leaf (hash of) is promoted to the upper layer.
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
extern crate alloc;
|
||||
#[cfg(not(feature = "std"))]
|
||||
use alloc::vec;
|
||||
#[cfg(not(feature = "std"))]
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::{RuntimeDebug, H256};
|
||||
use sp_runtime::traits::Hash;
|
||||
|
||||
/// Construct a root hash of a Binary Merkle Tree created from given leaves.
|
||||
///
|
||||
/// See crate-level docs for details about Merkle Tree construction.
|
||||
///
|
||||
/// In case an empty list of leaves is passed the function returns a 0-filled hash.
|
||||
pub fn merkle_root<H, I>(leaves: I) -> H256
|
||||
where
|
||||
H: Hash<Output = H256>,
|
||||
I: Iterator<Item = H256>,
|
||||
{
|
||||
merkelize::<H, _, _>(leaves, &mut ())
|
||||
}
|
||||
|
||||
fn merkelize<H, V, I>(leaves: I, visitor: &mut V) -> H256
|
||||
where
|
||||
H: Hash<Output = H256>,
|
||||
V: Visitor,
|
||||
I: Iterator<Item = H256>,
|
||||
{
|
||||
let upper = Vec::with_capacity(leaves.size_hint().0);
|
||||
let mut next = match merkelize_row::<H, _, _>(leaves, upper, visitor) {
|
||||
Ok(root) => return root,
|
||||
Err(next) if next.is_empty() => return H256::default(),
|
||||
Err(next) => next,
|
||||
};
|
||||
|
||||
let mut upper = Vec::with_capacity((next.len() + 1) / 2);
|
||||
loop {
|
||||
visitor.move_up();
|
||||
|
||||
match merkelize_row::<H, _, _>(next.drain(..), upper, visitor) {
|
||||
Ok(root) => return root,
|
||||
Err(t) => {
|
||||
// swap collections to avoid allocations
|
||||
upper = next;
|
||||
next = t;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// A generated merkle proof.
|
||||
///
|
||||
/// The structure contains all necessary data to later on verify the proof and the leaf itself.
|
||||
#[derive(Encode, Decode, RuntimeDebug, PartialEq, Eq, TypeInfo)]
|
||||
pub struct MerkleProof {
|
||||
/// Root hash of generated merkle tree.
|
||||
pub root: H256,
|
||||
/// Proof items (does not contain the leaf hash, nor the root obviously).
|
||||
///
|
||||
/// This vec contains all inner node hashes necessary to reconstruct the root hash given the
|
||||
/// leaf hash.
|
||||
pub proof: Vec<H256>,
|
||||
/// Number of leaves in the original tree.
|
||||
///
|
||||
/// This is needed to detect a case where we have an odd number of leaves that "get promoted"
|
||||
/// to upper layers.
|
||||
pub number_of_leaves: u64,
|
||||
/// Index of the leaf the proof is for (0-based).
|
||||
pub leaf_index: u64,
|
||||
/// Leaf content (hashed).
|
||||
pub leaf: H256,
|
||||
}
|
||||
|
||||
/// A trait of object inspecting merkle root creation.
|
||||
///
|
||||
/// It can be passed to [`merkelize_row`] or [`merkelize`] functions and will be notified
|
||||
/// about tree traversal.
|
||||
trait Visitor {
|
||||
/// We are moving one level up in the tree.
|
||||
fn move_up(&mut self);
|
||||
|
||||
/// We are creating an inner node from given `left` and `right` nodes.
|
||||
///
|
||||
/// Note that in case of last odd node in the row `right` might be empty.
|
||||
/// The method will also visit the `root` hash (level 0).
|
||||
///
|
||||
/// The `index` is an index of `left` item.
|
||||
fn visit(&mut self, index: u64, left: &Option<H256>, right: &Option<H256>);
|
||||
}
|
||||
|
||||
/// No-op implementation of the visitor.
|
||||
impl Visitor for () {
|
||||
fn move_up(&mut self) {}
|
||||
fn visit(&mut self, _index: u64, _left: &Option<H256>, _right: &Option<H256>) {}
|
||||
}
|
||||
|
||||
/// Construct a Merkle Proof for leaves given by indices.
|
||||
///
|
||||
/// The function constructs a (partial) Merkle Tree first and stores all elements required
|
||||
/// to prove the requested item (leaf) given the root hash.
|
||||
///
|
||||
/// Both the Proof and the Root Hash are returned.
|
||||
///
|
||||
/// # Panic
|
||||
///
|
||||
/// The function will panic if given `leaf_index` is greater than the number of leaves.
|
||||
pub fn merkle_proof<H, I>(leaves: I, leaf_index: u64) -> MerkleProof
|
||||
where
|
||||
H: Hash<Output = H256>,
|
||||
I: Iterator<Item = H256>,
|
||||
{
|
||||
let mut leaf = None;
|
||||
let mut hashes = vec![];
|
||||
let mut number_of_leaves = 0;
|
||||
for (idx, l) in (0u64..).zip(leaves) {
|
||||
// count the leaves
|
||||
number_of_leaves = idx + 1;
|
||||
hashes.push(l);
|
||||
// find the leaf for the proof
|
||||
if idx == leaf_index {
|
||||
leaf = Some(l);
|
||||
}
|
||||
}
|
||||
|
||||
/// The struct collects a proof for single leaf.
|
||||
struct ProofCollection {
|
||||
proof: Vec<H256>,
|
||||
position: u64,
|
||||
}
|
||||
|
||||
impl ProofCollection {
|
||||
fn new(position: u64) -> Self {
|
||||
ProofCollection { proof: Default::default(), position }
|
||||
}
|
||||
}
|
||||
|
||||
impl Visitor for ProofCollection {
|
||||
fn move_up(&mut self) {
|
||||
self.position /= 2;
|
||||
}
|
||||
|
||||
fn visit(&mut self, index: u64, left: &Option<H256>, right: &Option<H256>) {
|
||||
// we are at left branch - right goes to the proof.
|
||||
if self.position == index {
|
||||
if let Some(right) = right {
|
||||
self.proof.push(*right);
|
||||
}
|
||||
}
|
||||
// we are at right branch - left goes to the proof.
|
||||
if self.position == index + 1 {
|
||||
if let Some(left) = left {
|
||||
self.proof.push(*left);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut collect_proof = ProofCollection::new(leaf_index);
|
||||
|
||||
let root = merkelize::<H, _, _>(hashes.into_iter(), &mut collect_proof);
|
||||
let leaf = leaf.expect("Requested `leaf_index` is greater than number of leaves.");
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
log::debug!(
|
||||
"[merkle_proof] Proof: {:?}",
|
||||
collect_proof.proof.iter().map(hex::encode).collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
MerkleProof { root, proof: collect_proof.proof, number_of_leaves, leaf_index, leaf }
|
||||
}
|
||||
|
||||
/// Leaf node for proof verification.
|
||||
///
|
||||
/// Can be either a value that needs to be hashed first,
|
||||
/// or the hash itself.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Leaf<'a> {
|
||||
/// Leaf content.
|
||||
Value(&'a [u8]),
|
||||
/// Hash of the leaf content.
|
||||
Hash(H256),
|
||||
}
|
||||
|
||||
impl<'a, T: AsRef<[u8]>> From<&'a T> for Leaf<'a> {
|
||||
fn from(v: &'a T) -> Self {
|
||||
Leaf::Value(v.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<H256> for Leaf<'a> {
|
||||
fn from(v: H256) -> Self {
|
||||
Leaf::Hash(v)
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify Merkle Proof correctness versus given root hash.
|
||||
///
|
||||
/// The proof is NOT expected to contain leaf hash as the first
|
||||
/// element, but only all adjacent nodes required to eventually by process of
|
||||
/// concatenating and hashing end up with given root hash.
|
||||
///
|
||||
/// The proof must not contain the root hash.
|
||||
pub fn verify_proof<'a, H, P, L>(
|
||||
root: &'a H256,
|
||||
proof: P,
|
||||
number_of_leaves: u64,
|
||||
leaf_index: u64,
|
||||
leaf: L,
|
||||
) -> bool
|
||||
where
|
||||
H: Hash<Output = H256>,
|
||||
P: IntoIterator<Item = H256>,
|
||||
L: Into<Leaf<'a>>,
|
||||
{
|
||||
if leaf_index >= number_of_leaves {
|
||||
return false
|
||||
}
|
||||
|
||||
let leaf_hash = match leaf.into() {
|
||||
Leaf::Value(content) => <H as Hash>::hash(content),
|
||||
Leaf::Hash(hash) => hash,
|
||||
};
|
||||
|
||||
let hash_len = <H as sp_core::Hasher>::LENGTH;
|
||||
let mut combined = [0_u8; 64];
|
||||
let computed = proof.into_iter().fold(leaf_hash, |a, b| {
|
||||
if a < b {
|
||||
combined[..hash_len].copy_from_slice(a.as_ref());
|
||||
combined[hash_len..].copy_from_slice(b.as_ref());
|
||||
} else {
|
||||
combined[..hash_len].copy_from_slice(b.as_ref());
|
||||
combined[hash_len..].copy_from_slice(a.as_ref());
|
||||
}
|
||||
<H as Hash>::hash(&combined)
|
||||
});
|
||||
|
||||
root == &computed
|
||||
}
|
||||
|
||||
/// Processes a single row (layer) of a tree by taking pairs of elements,
|
||||
/// concatenating them, hashing and placing into resulting vector.
|
||||
///
|
||||
/// In case only one element is provided it is returned via `Ok` result, in any other case (also an
|
||||
/// empty iterator) an `Err` with the inner nodes of upper layer is returned.
|
||||
fn merkelize_row<H, V, I>(
|
||||
mut iter: I,
|
||||
mut next: Vec<H256>,
|
||||
visitor: &mut V,
|
||||
) -> Result<H256, Vec<H256>>
|
||||
where
|
||||
H: Hash<Output = H256>,
|
||||
V: Visitor,
|
||||
I: Iterator<Item = H256>,
|
||||
{
|
||||
#[cfg(feature = "debug")]
|
||||
log::debug!("[merkelize_row]");
|
||||
next.clear();
|
||||
|
||||
let hash_len = <H as sp_core::Hasher>::LENGTH;
|
||||
let mut index = 0;
|
||||
let mut combined = vec![0_u8; hash_len * 2];
|
||||
loop {
|
||||
let a = iter.next();
|
||||
let b = iter.next();
|
||||
visitor.visit(index, &a, &b);
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
log::debug!(" {:?}\n {:?}", a.as_ref().map(hex::encode), b.as_ref().map(hex::encode));
|
||||
|
||||
index += 2;
|
||||
match (a, b) {
|
||||
(Some(a), Some(b)) => {
|
||||
if a < b {
|
||||
combined[..hash_len].copy_from_slice(a.as_ref());
|
||||
combined[hash_len..].copy_from_slice(b.as_ref());
|
||||
} else {
|
||||
combined[..hash_len].copy_from_slice(b.as_ref());
|
||||
combined[hash_len..].copy_from_slice(a.as_ref());
|
||||
}
|
||||
|
||||
next.push(<H as Hash>::hash(&combined));
|
||||
},
|
||||
// Odd number of items. Promote the item to the upper layer.
|
||||
(Some(a), None) if !next.is_empty() => {
|
||||
next.push(a);
|
||||
},
|
||||
// Last item = root.
|
||||
(Some(a), None) => return Ok(a),
|
||||
// Finish up, no more items.
|
||||
_ => {
|
||||
#[cfg(feature = "debug")]
|
||||
log::debug!(
|
||||
"[merkelize_row] Next: {:?}",
|
||||
next.iter().map(hex::encode).collect::<Vec<_>>()
|
||||
);
|
||||
return Err(next)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use hex_literal::hex;
|
||||
use sp_core::keccak_256;
|
||||
use sp_runtime::traits::Keccak256;
|
||||
|
||||
fn make_leaves(count: u64) -> Vec<H256> {
|
||||
(0..count).map(|i| keccak_256(&i.to_le_bytes()).into()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_generate_empty_root() {
|
||||
// given
|
||||
let _ = env_logger::try_init();
|
||||
let data = vec![];
|
||||
|
||||
// when
|
||||
let out = merkle_root::<Keccak256, _>(data.into_iter());
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
hex::encode(out),
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_generate_single_root() {
|
||||
// given
|
||||
let _ = env_logger::try_init();
|
||||
let data = make_leaves(1);
|
||||
|
||||
// when
|
||||
let out = merkle_root::<Keccak256, _>(data.into_iter());
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
hex::encode(out),
|
||||
"011b4d03dd8c01f1049143cf9c4c817e4b167f1d1b83e5c6f0f10d89ba1e7bce"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_generate_root_pow_2() {
|
||||
// given
|
||||
let _ = env_logger::try_init();
|
||||
let data = make_leaves(2);
|
||||
|
||||
// when
|
||||
let out = merkle_root::<Keccak256, _>(data.into_iter());
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
hex::encode(out),
|
||||
"e497bd1c13b13a60af56fa0d2703517c232fde213ad20d2c3dd60735c6604512"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_generate_root_complex() {
|
||||
let _ = env_logger::try_init();
|
||||
let test = |root, data: Vec<H256>| {
|
||||
assert_eq!(
|
||||
array_bytes::bytes2hex("", merkle_root::<Keccak256, _>(data.into_iter()).as_ref()),
|
||||
root
|
||||
);
|
||||
};
|
||||
|
||||
test("816cc37bd8d39f7b0851838ebc875faf2afe58a03e95aca3b1333b3693f39dd3", make_leaves(3));
|
||||
|
||||
test("7501ea976cb92f305cca65ab11254589ea28bb8b59d3161506350adaa237d22f", make_leaves(4));
|
||||
|
||||
test("d26ba4eb398747bdd39255b1fadb99b803ce39696021b3b0bff7301ac146ee4e", make_leaves(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn should_generate_and_verify_proof() {
|
||||
// given
|
||||
let _ = env_logger::try_init();
|
||||
let data: Vec<H256> = make_leaves(3);
|
||||
|
||||
// when
|
||||
let proof0 = merkle_proof::<Keccak256, _>(data.clone().into_iter(), 0);
|
||||
assert!(verify_proof::<Keccak256, _, _>(
|
||||
&proof0.root,
|
||||
proof0.proof.clone(),
|
||||
data.len() as u64,
|
||||
proof0.leaf_index,
|
||||
&data[0],
|
||||
));
|
||||
|
||||
let proof1 = merkle_proof::<Keccak256, _>(data.clone().into_iter(), 1);
|
||||
assert!(verify_proof::<Keccak256, _, _>(
|
||||
&proof1.root,
|
||||
proof1.proof,
|
||||
data.len() as u64,
|
||||
proof1.leaf_index,
|
||||
&proof1.leaf,
|
||||
));
|
||||
|
||||
let proof2 = merkle_proof::<Keccak256, _>(data.clone().into_iter(), 2);
|
||||
assert!(verify_proof::<Keccak256, _, _>(
|
||||
&proof2.root,
|
||||
proof2.proof,
|
||||
data.len() as u64,
|
||||
proof2.leaf_index,
|
||||
&proof2.leaf
|
||||
));
|
||||
|
||||
// then
|
||||
assert_eq!(hex::encode(proof0.root), hex::encode(proof1.root));
|
||||
assert_eq!(hex::encode(proof2.root), hex::encode(proof1.root));
|
||||
|
||||
assert!(!verify_proof::<Keccak256, _, _>(
|
||||
&H256::from_slice(&hex!(
|
||||
"fb3b3be94be9e983ba5e094c9c51a7d96a4fa2e5d8e891df00ca89ba05bb1239"
|
||||
)),
|
||||
proof0.proof,
|
||||
data.len() as u64,
|
||||
proof0.leaf_index,
|
||||
&proof0.leaf
|
||||
));
|
||||
|
||||
assert!(!verify_proof::<Keccak256, _, _>(
|
||||
&proof0.root,
|
||||
vec![],
|
||||
data.len() as u64,
|
||||
proof0.leaf_index,
|
||||
&proof0.leaf
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn should_panic_on_invalid_leaf_index() {
|
||||
let _ = env_logger::try_init();
|
||||
merkle_proof::<Keccak256, _>(make_leaves(1).into_iter(), 5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "snowbridge-outbound-queue-runtime-api"
|
||||
description = "Snowbridge Outbound Queue Runtime API"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
repository = "https://github.com/Snowfork/snowbridge"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { version = "3.1.5", package = "parity-scale-codec", features = ["derive"], default-features = false }
|
||||
sp-core = { path = "../../../../../../substrate/primitives/core", default-features = false }
|
||||
sp-std = { path = "../../../../../../substrate/primitives/std", default-features = false }
|
||||
sp-api = { path = "../../../../../../substrate/primitives/api", default-features = false }
|
||||
frame-support = { path = "../../../../../../substrate/frame/support", default-features = false }
|
||||
xcm = { package = "staging-xcm", path = "../../../../../../polkadot/xcm", default-features = false }
|
||||
snowbridge-outbound-queue-merkle-tree = { path = "../merkle-tree", default-features = false }
|
||||
snowbridge-core = { path = "../../../primitives/core", default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"frame-support/std",
|
||||
"snowbridge-core/std",
|
||||
"snowbridge-outbound-queue-merkle-tree/std",
|
||||
"sp-api/std",
|
||||
"sp-core/std",
|
||||
"sp-std/std",
|
||||
"xcm/std",
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use frame_support::traits::tokens::Balance as BalanceT;
|
||||
use snowbridge_core::outbound::Message;
|
||||
use snowbridge_outbound_queue_merkle_tree::MerkleProof;
|
||||
|
||||
sp_api::decl_runtime_apis! {
|
||||
pub trait OutboundQueueApi<Balance> where Balance: BalanceT
|
||||
{
|
||||
/// Generate a merkle proof for a committed message identified by `leaf_index`.
|
||||
/// The merkle root is stored in the block header as a
|
||||
/// `\[`sp_runtime::generic::DigestItem::Other`\]`
|
||||
fn prove_message(leaf_index: u64) -> Option<MerkleProof>;
|
||||
|
||||
/// Calculate the delivery fee for `message`
|
||||
fn calculate_fee(message: Message) -> Option<Balance>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Helpers for implementing runtime api
|
||||
|
||||
use crate::{Config, MessageLeaves};
|
||||
use frame_support::storage::StorageStreamIter;
|
||||
use snowbridge_core::outbound::{Message, SendMessage};
|
||||
use snowbridge_outbound_queue_merkle_tree::{merkle_proof, MerkleProof};
|
||||
|
||||
pub fn prove_message<T>(leaf_index: u64) -> Option<MerkleProof>
|
||||
where
|
||||
T: Config,
|
||||
{
|
||||
if !MessageLeaves::<T>::exists() {
|
||||
return None
|
||||
}
|
||||
let proof =
|
||||
merkle_proof::<<T as Config>::Hashing, _>(MessageLeaves::<T>::stream_iter(), leaf_index);
|
||||
Some(proof)
|
||||
}
|
||||
|
||||
pub fn calculate_fee<T>(message: Message) -> Option<T::Balance>
|
||||
where
|
||||
T: Config,
|
||||
{
|
||||
match crate::Pallet::<T>::validate(&message) {
|
||||
Ok((_, fees)) => Some(fees.total()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use super::*;
|
||||
|
||||
use bridge_hub_common::AggregateMessageOrigin;
|
||||
use codec::Encode;
|
||||
use frame_benchmarking::v2::*;
|
||||
use snowbridge_core::{
|
||||
outbound::{Command, Initializer},
|
||||
ChannelId,
|
||||
};
|
||||
use sp_core::{H160, H256};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::Pallet as OutboundQueue;
|
||||
|
||||
#[benchmarks(
|
||||
where
|
||||
<T as Config>::MaxMessagePayloadSize: Get<u32>,
|
||||
)]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
/// Benchmark for processing a message.
|
||||
#[benchmark]
|
||||
fn do_process_message() -> Result<(), BenchmarkError> {
|
||||
let enqueued_message = QueuedMessage {
|
||||
id: H256::zero(),
|
||||
channel_id: ChannelId::from([1; 32]),
|
||||
command: Command::Upgrade {
|
||||
impl_address: H160::zero(),
|
||||
impl_code_hash: H256::zero(),
|
||||
initializer: Some(Initializer {
|
||||
params: [7u8; 256].into_iter().collect(),
|
||||
maximum_required_gas: 200_000,
|
||||
}),
|
||||
},
|
||||
};
|
||||
let origin = AggregateMessageOrigin::Snowbridge([1; 32].into());
|
||||
let encoded_enqueued_message = enqueued_message.encode();
|
||||
|
||||
#[block]
|
||||
{
|
||||
let _ = OutboundQueue::<T>::do_process_message(origin, &encoded_enqueued_message);
|
||||
}
|
||||
|
||||
assert_eq!(MessageLeaves::<T>::decode_len().unwrap(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Benchmark for producing final messages commitment
|
||||
#[benchmark]
|
||||
fn commit() -> Result<(), BenchmarkError> {
|
||||
// Assume worst case, where `MaxMessagesPerBlock` messages need to be committed.
|
||||
for i in 0..T::MaxMessagesPerBlock::get() {
|
||||
let leaf_data: [u8; 1] = [i as u8];
|
||||
let leaf = <T as Config>::Hashing::hash(&leaf_data);
|
||||
MessageLeaves::<T>::append(leaf);
|
||||
}
|
||||
|
||||
#[block]
|
||||
{
|
||||
OutboundQueue::<T>::commit();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Benchmark for producing commitment for a single message
|
||||
#[benchmark]
|
||||
fn commit_single() -> Result<(), BenchmarkError> {
|
||||
let leaf = <T as Config>::Hashing::hash(&[100; 1]);
|
||||
MessageLeaves::<T>::append(leaf);
|
||||
|
||||
#[block]
|
||||
{
|
||||
OutboundQueue::<T>::commit();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(OutboundQueue, crate::mock::new_tester(), crate::mock::Test,);
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Pallet for committing outbound messages for delivery to Ethereum
|
||||
//!
|
||||
//! # Overview
|
||||
//!
|
||||
//! Messages come either from sibling parachains via XCM, or BridgeHub itself
|
||||
//! via the `snowbridge-system` pallet:
|
||||
//!
|
||||
//! 1. `snowbridge_router_primitives::outbound::EthereumBlobExporter::deliver`
|
||||
//! 2. `snowbridge_system::Pallet::send`
|
||||
//!
|
||||
//! The message submission pipeline works like this:
|
||||
//! 1. The message is first validated via the implementation for
|
||||
//! [`snowbridge_core::outbound::SendMessage::validate`]
|
||||
//! 2. The message is then enqueued for later processing via the implementation for
|
||||
//! [`snowbridge_core::outbound::SendMessage::deliver`]
|
||||
//! 3. The underlying message queue is implemented by [`Config::MessageQueue`]
|
||||
//! 4. The message queue delivers messages back to this pallet via the implementation for
|
||||
//! [`frame_support::traits::ProcessMessage::process_message`]
|
||||
//! 5. The message is processed in `Pallet::do_process_message`: a. Assigned a nonce b. ABI-encoded,
|
||||
//! hashed, and stored in the `MessageLeaves` vector
|
||||
//! 6. At the end of the block, a merkle root is constructed from all the leaves in `MessageLeaves`.
|
||||
//! 7. This merkle root is inserted into the parachain header as a digest item
|
||||
//! 8. Offchain relayers are able to relay the message to Ethereum after: a. Generating a merkle
|
||||
//! proof for the committed message using the `prove_message` runtime API b. Reading the actual
|
||||
//! message content from the `Messages` vector in storage
|
||||
//!
|
||||
//! On the Ethereum side, the message root is ultimately the thing being
|
||||
//! verified by the Polkadot light client.
|
||||
//!
|
||||
//! # Message Priorities
|
||||
//!
|
||||
//! The processing of governance commands can never be halted. This effectively
|
||||
//! allows us to pause processing of normal user messages while still allowing
|
||||
//! governance commands to be sent to Ethereum.
|
||||
//!
|
||||
//! # Fees
|
||||
//!
|
||||
//! An upfront fee must be paid for delivering a message. This fee covers several
|
||||
//! components:
|
||||
//! 1. The weight of processing the message locally
|
||||
//! 2. The gas refund paid out to relayers for message submission
|
||||
//! 3. An additional reward paid out to relayers for message submission
|
||||
//!
|
||||
//! Messages are weighed to determine the maximum amount of gas they could
|
||||
//! consume on Ethereum. Using this upper bound, a final fee can be calculated.
|
||||
//!
|
||||
//! The fee calculation also requires the following parameters:
|
||||
//! * ETH/DOT exchange rate
|
||||
//! * Ether fee per unit of gas
|
||||
//!
|
||||
//! By design, it is expected that governance should manually update these
|
||||
//! parameters every few weeks using the `set_pricing_parameters` extrinsic in the
|
||||
//! system pallet.
|
||||
//!
|
||||
//! ## Fee Computation Function
|
||||
//!
|
||||
//! ```text
|
||||
//! LocalFee(Message) = WeightToFee(ProcessMessageWeight(Message))
|
||||
//! RemoteFee(Message) = MaxGasRequired(Message) * FeePerGas + Reward
|
||||
//! Fee(Message) = LocalFee(Message) + (RemoteFee(Message) / Ratio("ETH/DOT"))
|
||||
//! ```
|
||||
//!
|
||||
//! By design, the computed fee is always going to conservative, to cover worst-case
|
||||
//! costs of dispatch on Ethereum. In future iterations of the design, we will optimize
|
||||
//! this, or provide a mechanism to asynchronously refund a portion of collected fees.
|
||||
//!
|
||||
//! # Extrinsics
|
||||
//!
|
||||
//! * [`Call::set_operating_mode`]: Set the operating mode
|
||||
//!
|
||||
//! # Runtime API
|
||||
//!
|
||||
//! * `prove_message`: Generate a merkle proof for a committed message
|
||||
//! * `calculate_fee`: Calculate the delivery fee for a message
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
pub mod api;
|
||||
pub mod process_message_impl;
|
||||
pub mod send_message_impl;
|
||||
pub mod types;
|
||||
pub mod weights;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
use bridge_hub_common::{AggregateMessageOrigin, CustomDigestItem};
|
||||
use codec::Decode;
|
||||
use frame_support::{
|
||||
storage::StorageStreamIter,
|
||||
traits::{tokens::Balance, Contains, Defensive, EnqueueMessage, Get, ProcessMessageError},
|
||||
weights::{Weight, WeightToFee},
|
||||
};
|
||||
use snowbridge_core::{
|
||||
outbound::{Fee, GasMeter, QueuedMessage, VersionedQueuedMessage, ETHER_DECIMALS},
|
||||
BasicOperatingMode, ChannelId,
|
||||
};
|
||||
use snowbridge_outbound_queue_merkle_tree::merkle_root;
|
||||
pub use snowbridge_outbound_queue_merkle_tree::MerkleProof;
|
||||
use sp_core::{H256, U256};
|
||||
use sp_runtime::{
|
||||
traits::{CheckedDiv, Hash},
|
||||
DigestItem,
|
||||
};
|
||||
use sp_std::prelude::*;
|
||||
pub use types::{CommittedMessage, FeeConfigRecord, ProcessMessageOriginOf};
|
||||
pub use weights::WeightInfo;
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use frame_support::pallet_prelude::*;
|
||||
use frame_system::pallet_prelude::*;
|
||||
use snowbridge_core::PricingParameters;
|
||||
use sp_arithmetic::FixedU128;
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config {
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||||
|
||||
type Hashing: Hash<Output = H256>;
|
||||
|
||||
type MessageQueue: EnqueueMessage<AggregateMessageOrigin>;
|
||||
|
||||
/// Measures the maximum gas used to execute a command on Ethereum
|
||||
type GasMeter: GasMeter;
|
||||
|
||||
type Balance: Balance + From<u128>;
|
||||
|
||||
/// Number of decimal places in native currency
|
||||
#[pallet::constant]
|
||||
type Decimals: Get<u8>;
|
||||
|
||||
/// Max bytes in a message payload
|
||||
#[pallet::constant]
|
||||
type MaxMessagePayloadSize: Get<u32>;
|
||||
|
||||
/// Max number of messages processed per block
|
||||
#[pallet::constant]
|
||||
type MaxMessagesPerBlock: Get<u32>;
|
||||
|
||||
/// Check whether a channel exists
|
||||
type Channels: Contains<ChannelId>;
|
||||
|
||||
type PricingParameters: Get<PricingParameters<Self::Balance>>;
|
||||
|
||||
/// Convert a weight value into a deductible fee based.
|
||||
type WeightToFee: WeightToFee<Balance = Self::Balance>;
|
||||
|
||||
/// Weight information for extrinsics in this pallet
|
||||
type WeightInfo: WeightInfo;
|
||||
}
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// Message has been queued and will be processed in the future
|
||||
MessageQueued {
|
||||
/// ID of the message. Usually the XCM message hash or a SetTopic.
|
||||
id: H256,
|
||||
},
|
||||
/// Message will be committed at the end of current block. From now on, to track the
|
||||
/// progress the message, use the `nonce` of `id`.
|
||||
MessageAccepted {
|
||||
/// ID of the message
|
||||
id: H256,
|
||||
/// The nonce assigned to this message
|
||||
nonce: u64,
|
||||
},
|
||||
/// Some messages have been committed
|
||||
MessagesCommitted {
|
||||
/// Merkle root of the committed messages
|
||||
root: H256,
|
||||
/// number of committed messages
|
||||
count: u64,
|
||||
},
|
||||
/// Set OperatingMode
|
||||
OperatingModeChanged {
|
||||
mode: BasicOperatingMode,
|
||||
},
|
||||
FeeConfigChanged {
|
||||
fee_config: FeeConfigRecord,
|
||||
},
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// The message is too large
|
||||
MessageTooLarge,
|
||||
/// The pallet is halted
|
||||
Halted,
|
||||
// Invalid fee config
|
||||
InvalidFeeConfig,
|
||||
/// Invalid Channel
|
||||
InvalidChannel,
|
||||
}
|
||||
|
||||
/// Messages to be committed in the current block. This storage value is killed in
|
||||
/// `on_initialize`, so should never go into block PoV.
|
||||
///
|
||||
/// Is never read in the runtime, only by offchain message relayers.
|
||||
///
|
||||
/// Inspired by the `frame_system::Pallet::Events` storage value
|
||||
#[pallet::storage]
|
||||
#[pallet::unbounded]
|
||||
pub(super) type Messages<T: Config> = StorageValue<_, Vec<CommittedMessage>, ValueQuery>;
|
||||
|
||||
/// Hashes of the ABI-encoded messages in the [`Messages`] storage value. Used to generate a
|
||||
/// merkle root during `on_finalize`. This storage value is killed in
|
||||
/// `on_initialize`, so should never go into block PoV.
|
||||
#[pallet::storage]
|
||||
#[pallet::unbounded]
|
||||
#[pallet::getter(fn message_leaves)]
|
||||
pub(super) type MessageLeaves<T: Config> = StorageValue<_, Vec<H256>, ValueQuery>;
|
||||
|
||||
/// The current nonce for each message origin
|
||||
#[pallet::storage]
|
||||
pub type Nonce<T: Config> = StorageMap<_, Twox64Concat, ChannelId, u64, ValueQuery>;
|
||||
|
||||
/// The current operating mode of the pallet.
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn operating_mode)]
|
||||
pub type OperatingMode<T: Config> = StorageValue<_, BasicOperatingMode, ValueQuery>;
|
||||
|
||||
#[pallet::hooks]
|
||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T>
|
||||
where
|
||||
T::AccountId: AsRef<[u8]>,
|
||||
{
|
||||
fn on_initialize(_: BlockNumberFor<T>) -> Weight {
|
||||
// Remove storage from previous block
|
||||
Messages::<T>::kill();
|
||||
MessageLeaves::<T>::kill();
|
||||
// Reserve some weight for the `on_finalize` handler
|
||||
T::WeightInfo::commit()
|
||||
}
|
||||
|
||||
fn on_finalize(_: BlockNumberFor<T>) {
|
||||
Self::commit();
|
||||
}
|
||||
|
||||
fn integrity_test() {
|
||||
let decimals = T::Decimals::get();
|
||||
assert!(decimals == 10 || decimals == 12, "Decimals should be 10 or 12");
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Halt or resume all pallet operations. May only be called by root.
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
|
||||
pub fn set_operating_mode(
|
||||
origin: OriginFor<T>,
|
||||
mode: BasicOperatingMode,
|
||||
) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
OperatingMode::<T>::put(mode);
|
||||
Self::deposit_event(Event::OperatingModeChanged { mode });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Generate a messages commitment and insert it into the header digest
|
||||
pub(crate) fn commit() {
|
||||
let count = MessageLeaves::<T>::decode_len().unwrap_or_default() as u64;
|
||||
if count == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Create merkle root of messages
|
||||
let root = merkle_root::<<T as Config>::Hashing, _>(MessageLeaves::<T>::stream_iter());
|
||||
|
||||
let digest_item: DigestItem = CustomDigestItem::Snowbridge(root).into();
|
||||
|
||||
// Insert merkle root into the header digest
|
||||
<frame_system::Pallet<T>>::deposit_log(digest_item);
|
||||
|
||||
Self::deposit_event(Event::MessagesCommitted { root, count });
|
||||
}
|
||||
|
||||
/// Process a message delivered by the MessageQueue pallet
|
||||
pub(crate) fn do_process_message(
|
||||
_: ProcessMessageOriginOf<T>,
|
||||
mut message: &[u8],
|
||||
) -> Result<bool, ProcessMessageError> {
|
||||
use ProcessMessageError::*;
|
||||
|
||||
// Yield if the maximum number of messages has been processed this block.
|
||||
// This ensures that the weight of `on_finalize` has a known maximum bound.
|
||||
ensure!(
|
||||
MessageLeaves::<T>::decode_len().unwrap_or(0) <
|
||||
T::MaxMessagesPerBlock::get() as usize,
|
||||
Yield
|
||||
);
|
||||
|
||||
// Decode bytes into versioned message
|
||||
let versioned_queued_message: VersionedQueuedMessage =
|
||||
VersionedQueuedMessage::decode(&mut message).map_err(|_| Corrupt)?;
|
||||
|
||||
// Convert versioned message into latest supported message version
|
||||
let queued_message: QueuedMessage =
|
||||
versioned_queued_message.try_into().map_err(|_| Unsupported)?;
|
||||
|
||||
// Obtain next nonce
|
||||
let nonce = <Nonce<T>>::try_mutate(
|
||||
queued_message.channel_id,
|
||||
|nonce| -> Result<u64, ProcessMessageError> {
|
||||
*nonce = nonce.checked_add(1).ok_or(Unsupported)?;
|
||||
Ok(*nonce)
|
||||
},
|
||||
)?;
|
||||
|
||||
let pricing_params = T::PricingParameters::get();
|
||||
let command = queued_message.command.index();
|
||||
let params = queued_message.command.abi_encode();
|
||||
let max_dispatch_gas =
|
||||
T::GasMeter::maximum_dispatch_gas_used_at_most(&queued_message.command);
|
||||
let reward = pricing_params.rewards.remote;
|
||||
|
||||
// Construct the final committed message
|
||||
let message = CommittedMessage {
|
||||
channel_id: queued_message.channel_id,
|
||||
nonce,
|
||||
command,
|
||||
params,
|
||||
max_dispatch_gas,
|
||||
max_fee_per_gas: pricing_params
|
||||
.fee_per_gas
|
||||
.try_into()
|
||||
.defensive_unwrap_or(u128::MAX),
|
||||
reward: reward.try_into().defensive_unwrap_or(u128::MAX),
|
||||
id: queued_message.id,
|
||||
};
|
||||
|
||||
// ABI-encode and hash the prepared message
|
||||
let message_abi_encoded = ethabi::encode(&[message.clone().into()]);
|
||||
let message_abi_encoded_hash = <T as Config>::Hashing::hash(&message_abi_encoded);
|
||||
|
||||
Messages::<T>::append(Box::new(message));
|
||||
MessageLeaves::<T>::append(message_abi_encoded_hash);
|
||||
|
||||
Self::deposit_event(Event::MessageAccepted { id: queued_message.id, nonce });
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Calculate total fee in native currency to cover all costs of delivering a message to the
|
||||
/// remote destination. See module-level documentation for more details.
|
||||
pub(crate) fn calculate_fee(
|
||||
gas_used_at_most: u64,
|
||||
params: PricingParameters<T::Balance>,
|
||||
) -> Fee<T::Balance> {
|
||||
// Remote fee in ether
|
||||
let fee = Self::calculate_remote_fee(
|
||||
gas_used_at_most,
|
||||
params.fee_per_gas,
|
||||
params.rewards.remote,
|
||||
);
|
||||
|
||||
// downcast to u128
|
||||
let fee: u128 = fee.try_into().defensive_unwrap_or(u128::MAX);
|
||||
|
||||
// convert to local currency
|
||||
let fee = FixedU128::from_inner(fee)
|
||||
.checked_div(¶ms.exchange_rate)
|
||||
.expect("exchange rate is not zero; qed")
|
||||
.into_inner();
|
||||
|
||||
// adjust fixed point to match local currency
|
||||
let fee = Self::convert_from_ether_decimals(fee);
|
||||
|
||||
Fee::from((Self::calculate_local_fee(), fee))
|
||||
}
|
||||
|
||||
/// Calculate fee in remote currency for dispatching a message on Ethereum
|
||||
pub(crate) fn calculate_remote_fee(
|
||||
gas_used_at_most: u64,
|
||||
fee_per_gas: U256,
|
||||
reward: U256,
|
||||
) -> U256 {
|
||||
fee_per_gas.saturating_mul(gas_used_at_most.into()).saturating_add(reward)
|
||||
}
|
||||
|
||||
/// The local component of the message processing fees in native currency
|
||||
pub(crate) fn calculate_local_fee() -> T::Balance {
|
||||
T::WeightToFee::weight_to_fee(
|
||||
&T::WeightInfo::do_process_message().saturating_add(T::WeightInfo::commit_single()),
|
||||
)
|
||||
}
|
||||
|
||||
// 1 DOT has 10 digits of precision
|
||||
// 1 KSM has 12 digits of precision
|
||||
// 1 ETH has 18 digits of precision
|
||||
pub(crate) fn convert_from_ether_decimals(value: u128) -> T::Balance {
|
||||
let decimals = ETHER_DECIMALS.saturating_sub(T::Decimals::get()) as u32;
|
||||
let denom = 10u128.saturating_pow(decimals);
|
||||
value.checked_div(denom).expect("divisor is non-zero; qed").into()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use super::*;
|
||||
|
||||
use frame_support::{
|
||||
parameter_types,
|
||||
traits::{Everything, Hooks},
|
||||
weights::IdentityFee,
|
||||
};
|
||||
|
||||
use snowbridge_core::{
|
||||
gwei, meth,
|
||||
outbound::*,
|
||||
pricing::{PricingParameters, Rewards},
|
||||
ParaId, PRIMARY_GOVERNANCE_CHANNEL,
|
||||
};
|
||||
use sp_core::{ConstU32, ConstU8, H160, H256};
|
||||
use sp_runtime::{
|
||||
traits::{BlakeTwo256, IdentityLookup, Keccak256},
|
||||
AccountId32, BuildStorage, FixedU128,
|
||||
};
|
||||
use sp_std::marker::PhantomData;
|
||||
|
||||
type Block = frame_system::mocking::MockBlock<Test>;
|
||||
type AccountId = AccountId32;
|
||||
|
||||
frame_support::construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: frame_system::{Pallet, Call, Storage, Event<T>},
|
||||
MessageQueue: pallet_message_queue::{Pallet, Call, Storage, Event<T>},
|
||||
OutboundQueue: crate::{Pallet, Storage, Event<T>},
|
||||
}
|
||||
);
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
}
|
||||
|
||||
impl frame_system::Config for Test {
|
||||
type BaseCallFilter = Everything;
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type RuntimeTask = RuntimeTask;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type DbWeight = ();
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = ();
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = frame_support::traits::ConstU32<16>;
|
||||
type Nonce = u64;
|
||||
type Block = Block;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const HeapSize: u32 = 32 * 1024;
|
||||
pub const MaxStale: u32 = 32;
|
||||
pub static ServiceWeight: Option<Weight> = Some(Weight::from_parts(100, 100));
|
||||
}
|
||||
|
||||
impl pallet_message_queue::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type WeightInfo = ();
|
||||
type MessageProcessor = OutboundQueue;
|
||||
type Size = u32;
|
||||
type QueueChangeHandler = ();
|
||||
type HeapSize = HeapSize;
|
||||
type MaxStale = MaxStale;
|
||||
type ServiceWeight = ServiceWeight;
|
||||
type QueuePausedQuery = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const OwnParaId: ParaId = ParaId::new(1013);
|
||||
pub Parameters: PricingParameters<u128> = PricingParameters {
|
||||
exchange_rate: FixedU128::from_rational(1, 400),
|
||||
fee_per_gas: gwei(20),
|
||||
rewards: Rewards { local: DOT, remote: meth(1) }
|
||||
};
|
||||
}
|
||||
|
||||
pub const DOT: u128 = 10_000_000_000;
|
||||
|
||||
impl crate::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Hashing = Keccak256;
|
||||
type MessageQueue = MessageQueue;
|
||||
type Decimals = ConstU8<12>;
|
||||
type MaxMessagePayloadSize = ConstU32<1024>;
|
||||
type MaxMessagesPerBlock = ConstU32<20>;
|
||||
type GasMeter = ConstantGasMeter;
|
||||
type Balance = u128;
|
||||
type PricingParameters = Parameters;
|
||||
type Channels = Everything;
|
||||
type WeightToFee = IdentityFee<u128>;
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
fn setup() {
|
||||
System::set_block_number(1);
|
||||
}
|
||||
|
||||
pub fn new_tester() -> sp_io::TestExternalities {
|
||||
let storage = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
let mut ext: sp_io::TestExternalities = storage.into();
|
||||
ext.execute_with(setup);
|
||||
ext
|
||||
}
|
||||
|
||||
pub fn run_to_end_of_next_block() {
|
||||
// finish current block
|
||||
MessageQueue::on_finalize(System::block_number());
|
||||
OutboundQueue::on_finalize(System::block_number());
|
||||
System::on_finalize(System::block_number());
|
||||
// start next block
|
||||
System::set_block_number(System::block_number() + 1);
|
||||
System::on_initialize(System::block_number());
|
||||
OutboundQueue::on_initialize(System::block_number());
|
||||
MessageQueue::on_initialize(System::block_number());
|
||||
// finish next block
|
||||
MessageQueue::on_finalize(System::block_number());
|
||||
OutboundQueue::on_finalize(System::block_number());
|
||||
System::on_finalize(System::block_number());
|
||||
}
|
||||
|
||||
pub fn mock_governance_message<T>() -> Message
|
||||
where
|
||||
T: Config,
|
||||
{
|
||||
let _marker = PhantomData::<T>; // for clippy
|
||||
|
||||
Message {
|
||||
id: None,
|
||||
channel_id: PRIMARY_GOVERNANCE_CHANNEL,
|
||||
command: Command::Upgrade {
|
||||
impl_address: H160::zero(),
|
||||
impl_code_hash: H256::zero(),
|
||||
initializer: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Message should fail validation as it is too large
|
||||
pub fn mock_invalid_governance_message<T>() -> Message
|
||||
where
|
||||
T: Config,
|
||||
{
|
||||
let _marker = PhantomData::<T>; // for clippy
|
||||
|
||||
Message {
|
||||
id: None,
|
||||
channel_id: PRIMARY_GOVERNANCE_CHANNEL,
|
||||
command: Command::Upgrade {
|
||||
impl_address: H160::zero(),
|
||||
impl_code_hash: H256::zero(),
|
||||
initializer: Some(Initializer {
|
||||
params: (0..1000).map(|_| 1u8).collect::<Vec<u8>>(),
|
||||
maximum_required_gas: 0,
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mock_message(sibling_para_id: u32) -> Message {
|
||||
Message {
|
||||
id: None,
|
||||
channel_id: ParaId::from(sibling_para_id).into(),
|
||||
command: Command::AgentExecute {
|
||||
agent_id: Default::default(),
|
||||
command: AgentExecuteCommand::TransferToken {
|
||||
token: Default::default(),
|
||||
recipient: Default::default(),
|
||||
amount: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//! Implementation for [`frame_support::traits::ProcessMessage`]
|
||||
use super::*;
|
||||
use crate::weights::WeightInfo;
|
||||
use frame_support::{
|
||||
traits::{ProcessMessage, ProcessMessageError},
|
||||
weights::WeightMeter,
|
||||
};
|
||||
|
||||
impl<T: Config> ProcessMessage for Pallet<T> {
|
||||
type Origin = AggregateMessageOrigin;
|
||||
fn process_message(
|
||||
message: &[u8],
|
||||
origin: Self::Origin,
|
||||
meter: &mut WeightMeter,
|
||||
_: &mut [u8; 32],
|
||||
) -> Result<bool, ProcessMessageError> {
|
||||
let weight = T::WeightInfo::do_process_message();
|
||||
if meter.try_consume(weight).is_err() {
|
||||
return Err(ProcessMessageError::Overweight(weight))
|
||||
}
|
||||
Self::do_process_message(origin, message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
//! Implementation for [`snowbridge_core::outbound::SendMessage`]
|
||||
use super::*;
|
||||
use bridge_hub_common::AggregateMessageOrigin;
|
||||
use codec::Encode;
|
||||
use frame_support::{
|
||||
ensure,
|
||||
traits::{EnqueueMessage, Get},
|
||||
CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound,
|
||||
};
|
||||
use frame_system::unique;
|
||||
use snowbridge_core::{
|
||||
outbound::{
|
||||
Fee, Message, QueuedMessage, SendError, SendMessage, SendMessageFeeProvider,
|
||||
VersionedQueuedMessage,
|
||||
},
|
||||
ChannelId, PRIMARY_GOVERNANCE_CHANNEL,
|
||||
};
|
||||
use sp_core::H256;
|
||||
use sp_runtime::BoundedVec;
|
||||
|
||||
/// The maximal length of an enqueued message, as determined by the MessageQueue pallet
|
||||
pub type MaxEnqueuedMessageSizeOf<T> =
|
||||
<<T as Config>::MessageQueue as EnqueueMessage<AggregateMessageOrigin>>::MaxMessageLen;
|
||||
|
||||
#[derive(Encode, Decode, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound)]
|
||||
pub struct Ticket<T>
|
||||
where
|
||||
T: Config,
|
||||
{
|
||||
pub message_id: H256,
|
||||
pub channel_id: ChannelId,
|
||||
pub message: BoundedVec<u8, MaxEnqueuedMessageSizeOf<T>>,
|
||||
}
|
||||
|
||||
impl<T> SendMessage for Pallet<T>
|
||||
where
|
||||
T: Config,
|
||||
{
|
||||
type Ticket = Ticket<T>;
|
||||
|
||||
fn validate(
|
||||
message: &Message,
|
||||
) -> Result<(Self::Ticket, Fee<<Self as SendMessageFeeProvider>::Balance>), SendError> {
|
||||
// The inner payload should not be too large
|
||||
let payload = message.command.abi_encode();
|
||||
ensure!(
|
||||
payload.len() < T::MaxMessagePayloadSize::get() as usize,
|
||||
SendError::MessageTooLarge
|
||||
);
|
||||
|
||||
// Ensure there is a registered channel we can transmit this message on
|
||||
ensure!(T::Channels::contains(&message.channel_id), SendError::InvalidChannel);
|
||||
|
||||
// Generate a unique message id unless one is provided
|
||||
let message_id: H256 = message
|
||||
.id
|
||||
.unwrap_or_else(|| unique((message.channel_id, &message.command)).into());
|
||||
|
||||
let gas_used_at_most = T::GasMeter::maximum_gas_used_at_most(&message.command);
|
||||
let fee = Self::calculate_fee(gas_used_at_most, T::PricingParameters::get());
|
||||
|
||||
let queued_message: VersionedQueuedMessage = QueuedMessage {
|
||||
id: message_id,
|
||||
channel_id: message.channel_id,
|
||||
command: message.command.clone(),
|
||||
}
|
||||
.into();
|
||||
// The whole message should not be too large
|
||||
let encoded = queued_message.encode().try_into().map_err(|_| SendError::MessageTooLarge)?;
|
||||
|
||||
let ticket = Ticket { message_id, channel_id: message.channel_id, message: encoded };
|
||||
|
||||
Ok((ticket, fee))
|
||||
}
|
||||
|
||||
fn deliver(ticket: Self::Ticket) -> Result<H256, SendError> {
|
||||
let origin = AggregateMessageOrigin::Snowbridge(ticket.channel_id);
|
||||
|
||||
if ticket.channel_id != PRIMARY_GOVERNANCE_CHANNEL {
|
||||
ensure!(!Self::operating_mode().is_halted(), SendError::Halted);
|
||||
}
|
||||
|
||||
let message = ticket.message.as_bounded_slice();
|
||||
|
||||
T::MessageQueue::enqueue_message(message, origin);
|
||||
Self::deposit_event(Event::MessageQueued { id: ticket.message_id });
|
||||
Ok(ticket.message_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> SendMessageFeeProvider for Pallet<T> {
|
||||
type Balance = T::Balance;
|
||||
|
||||
/// The local component of the message processing fees in native currency
|
||||
fn local_fee() -> Self::Balance {
|
||||
Self::calculate_local_fee()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use crate::{mock::*, *};
|
||||
|
||||
use frame_support::{
|
||||
assert_err, assert_noop, assert_ok,
|
||||
traits::{Hooks, ProcessMessage, ProcessMessageError},
|
||||
weights::WeightMeter,
|
||||
};
|
||||
|
||||
use codec::Encode;
|
||||
use snowbridge_core::{
|
||||
outbound::{Command, SendError, SendMessage},
|
||||
ParaId,
|
||||
};
|
||||
use sp_arithmetic::FixedU128;
|
||||
use sp_core::H256;
|
||||
use sp_runtime::FixedPointNumber;
|
||||
|
||||
#[test]
|
||||
fn submit_messages_and_commit() {
|
||||
new_tester().execute_with(|| {
|
||||
for para_id in 1000..1004 {
|
||||
let message = mock_message(para_id);
|
||||
let (ticket, _) = OutboundQueue::validate(&message).unwrap();
|
||||
assert_ok!(OutboundQueue::deliver(ticket));
|
||||
}
|
||||
|
||||
ServiceWeight::set(Some(Weight::MAX));
|
||||
run_to_end_of_next_block();
|
||||
|
||||
for para_id in 1000..1004 {
|
||||
let origin: ParaId = (para_id as u32).into();
|
||||
let channel_id: ChannelId = origin.into();
|
||||
assert_eq!(Nonce::<Test>::get(channel_id), 1);
|
||||
}
|
||||
|
||||
let digest = System::digest();
|
||||
let digest_items = digest.logs();
|
||||
assert!(digest_items.len() == 1 && digest_items[0].as_other().is_some());
|
||||
assert_eq!(Messages::<Test>::decode_len(), Some(4));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submit_message_fail_too_large() {
|
||||
new_tester().execute_with(|| {
|
||||
let message = mock_invalid_governance_message::<Test>();
|
||||
assert_err!(OutboundQueue::validate(&message), SendError::MessageTooLarge);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_from_ether_decimals() {
|
||||
assert_eq!(
|
||||
OutboundQueue::convert_from_ether_decimals(1_000_000_000_000_000_000),
|
||||
1_000_000_000_000
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commit_exits_early_if_no_processed_messages() {
|
||||
new_tester().execute_with(|| {
|
||||
// on_finalize should do nothing, nor should it panic
|
||||
OutboundQueue::on_finalize(System::block_number());
|
||||
|
||||
let digest = System::digest();
|
||||
let digest_items = digest.logs();
|
||||
assert_eq!(digest_items.len(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_message_yields_on_max_messages_per_block() {
|
||||
new_tester().execute_with(|| {
|
||||
for _ in 0..<Test as Config>::MaxMessagesPerBlock::get() {
|
||||
MessageLeaves::<Test>::append(H256::zero())
|
||||
}
|
||||
|
||||
let channel_id: ChannelId = ParaId::from(1000).into();
|
||||
let origin = AggregateMessageOrigin::Snowbridge(channel_id);
|
||||
let message = QueuedMessage {
|
||||
id: Default::default(),
|
||||
channel_id,
|
||||
command: Command::Upgrade {
|
||||
impl_address: Default::default(),
|
||||
impl_code_hash: Default::default(),
|
||||
initializer: None,
|
||||
},
|
||||
}
|
||||
.encode();
|
||||
|
||||
let mut meter = WeightMeter::new();
|
||||
|
||||
assert_noop!(
|
||||
OutboundQueue::process_message(message.as_slice(), origin, &mut meter, &mut [0u8; 32]),
|
||||
ProcessMessageError::Yield
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_message_fails_on_max_nonce_reached() {
|
||||
new_tester().execute_with(|| {
|
||||
let sibling_id = 1000;
|
||||
let channel_id: ChannelId = ParaId::from(sibling_id).into();
|
||||
let origin = AggregateMessageOrigin::Snowbridge(channel_id);
|
||||
let message: QueuedMessage = QueuedMessage {
|
||||
id: H256::zero(),
|
||||
channel_id,
|
||||
command: mock_message(sibling_id).command,
|
||||
};
|
||||
let versioned_queued_message: VersionedQueuedMessage = message.try_into().unwrap();
|
||||
let encoded = versioned_queued_message.encode();
|
||||
let mut meter = WeightMeter::with_limit(Weight::MAX);
|
||||
|
||||
Nonce::<Test>::set(channel_id, u64::MAX);
|
||||
|
||||
assert_noop!(
|
||||
OutboundQueue::process_message(encoded.as_slice(), origin, &mut meter, &mut [0u8; 32]),
|
||||
ProcessMessageError::Unsupported
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_message_fails_on_overweight_message() {
|
||||
new_tester().execute_with(|| {
|
||||
let sibling_id = 1000;
|
||||
let channel_id: ChannelId = ParaId::from(sibling_id).into();
|
||||
let origin = AggregateMessageOrigin::Snowbridge(channel_id);
|
||||
let message: QueuedMessage = QueuedMessage {
|
||||
id: H256::zero(),
|
||||
channel_id,
|
||||
command: mock_message(sibling_id).command,
|
||||
};
|
||||
let versioned_queued_message: VersionedQueuedMessage = message.try_into().unwrap();
|
||||
let encoded = versioned_queued_message.encode();
|
||||
let mut meter = WeightMeter::with_limit(Weight::from_parts(1, 1));
|
||||
assert_noop!(
|
||||
OutboundQueue::process_message(encoded.as_slice(), origin, &mut meter, &mut [0u8; 32]),
|
||||
ProcessMessageError::Overweight(<Test as Config>::WeightInfo::do_process_message())
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
// Governance messages should be able to bypass a halted operating mode
|
||||
// Other message sends should fail when halted
|
||||
#[test]
|
||||
fn submit_upgrade_message_success_when_queue_halted() {
|
||||
new_tester().execute_with(|| {
|
||||
// halt the outbound queue
|
||||
OutboundQueue::set_operating_mode(RuntimeOrigin::root(), BasicOperatingMode::Halted)
|
||||
.unwrap();
|
||||
|
||||
// submit a high priority message from bridge_hub should success
|
||||
let message = mock_governance_message::<Test>();
|
||||
let (ticket, _) = OutboundQueue::validate(&message).unwrap();
|
||||
assert_ok!(OutboundQueue::deliver(ticket));
|
||||
|
||||
// submit a low priority message from asset_hub will fail as pallet is halted
|
||||
let message = mock_message(1000);
|
||||
let (ticket, _) = OutboundQueue::validate(&message).unwrap();
|
||||
assert_noop!(OutboundQueue::deliver(ticket), SendError::Halted);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn governance_message_does_not_get_the_chance_to_processed_in_same_block_when_congest_of_low_priority_sibling_messages(
|
||||
) {
|
||||
use snowbridge_core::PRIMARY_GOVERNANCE_CHANNEL;
|
||||
use AggregateMessageOrigin::*;
|
||||
|
||||
let sibling_id: u32 = 1000;
|
||||
let sibling_channel_id: ChannelId = ParaId::from(sibling_id).into();
|
||||
|
||||
new_tester().execute_with(|| {
|
||||
// submit a lot of low priority messages from asset_hub which will need multiple blocks to
|
||||
// execute(20 messages for each block so 40 required at least 2 blocks)
|
||||
let max_messages = 40;
|
||||
for _ in 0..max_messages {
|
||||
// submit low priority message
|
||||
let message = mock_message(sibling_id);
|
||||
let (ticket, _) = OutboundQueue::validate(&message).unwrap();
|
||||
OutboundQueue::deliver(ticket).unwrap();
|
||||
}
|
||||
|
||||
let footprint = MessageQueue::footprint(Snowbridge(sibling_channel_id));
|
||||
assert_eq!(footprint.storage.count, (max_messages) as u64);
|
||||
|
||||
let message = mock_governance_message::<Test>();
|
||||
let (ticket, _) = OutboundQueue::validate(&message).unwrap();
|
||||
OutboundQueue::deliver(ticket).unwrap();
|
||||
|
||||
// move to next block
|
||||
ServiceWeight::set(Some(Weight::MAX));
|
||||
run_to_end_of_next_block();
|
||||
|
||||
// first process 20 messages from sibling channel
|
||||
let footprint = MessageQueue::footprint(Snowbridge(sibling_channel_id));
|
||||
assert_eq!(footprint.storage.count, 40 - 20);
|
||||
|
||||
// and governance message does not have the chance to execute in same block
|
||||
let footprint = MessageQueue::footprint(Snowbridge(PRIMARY_GOVERNANCE_CHANNEL));
|
||||
assert_eq!(footprint.storage.count, 1);
|
||||
|
||||
// move to next block
|
||||
ServiceWeight::set(Some(Weight::MAX));
|
||||
run_to_end_of_next_block();
|
||||
|
||||
// now governance message get executed in this block
|
||||
let footprint = MessageQueue::footprint(Snowbridge(PRIMARY_GOVERNANCE_CHANNEL));
|
||||
assert_eq!(footprint.storage.count, 0);
|
||||
|
||||
// and this time process 19 messages from sibling channel so we have 1 message left
|
||||
let footprint = MessageQueue::footprint(Snowbridge(sibling_channel_id));
|
||||
assert_eq!(footprint.storage.count, 1);
|
||||
|
||||
// move to the next block, the last 1 message from sibling channel get executed
|
||||
ServiceWeight::set(Some(Weight::MAX));
|
||||
run_to_end_of_next_block();
|
||||
let footprint = MessageQueue::footprint(Snowbridge(sibling_channel_id));
|
||||
assert_eq!(footprint.storage.count, 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_local_currency() {
|
||||
new_tester().execute_with(|| {
|
||||
let fee: u128 = 1_000_000;
|
||||
let fee1 = FixedU128::from_inner(fee).into_inner();
|
||||
let fee2 = FixedU128::from(fee)
|
||||
.into_inner()
|
||||
.checked_div(FixedU128::accuracy())
|
||||
.expect("accuracy is not zero; qed");
|
||||
assert_eq!(fee, fee1);
|
||||
assert_eq!(fee, fee2);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_digest_item_with_correct_index() {
|
||||
new_tester().execute_with(|| {
|
||||
let digest_item: DigestItem = CustomDigestItem::Snowbridge(H256::default()).into();
|
||||
let enum_prefix = match digest_item {
|
||||
DigestItem::Other(data) => data[0],
|
||||
_ => u8::MAX,
|
||||
};
|
||||
assert_eq!(enum_prefix, 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_digest_item() {
|
||||
new_tester().execute_with(|| {
|
||||
let digest_item: DigestItem = CustomDigestItem::Snowbridge([5u8; 32].into()).into();
|
||||
let digest_item_raw = digest_item.encode();
|
||||
assert_eq!(digest_item_raw[0], 0); // DigestItem::Other
|
||||
assert_eq!(digest_item_raw[2], 0); // CustomDigestItem::Snowbridge
|
||||
assert_eq!(
|
||||
digest_item_raw,
|
||||
[
|
||||
0, 132, 0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
|
||||
5, 5, 5, 5, 5, 5, 5, 5
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use ethabi::Token;
|
||||
use frame_support::traits::ProcessMessage;
|
||||
use scale_info::TypeInfo;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sp_arithmetic::FixedU128;
|
||||
use sp_core::H256;
|
||||
use sp_runtime::{traits::Zero, RuntimeDebug};
|
||||
use sp_std::prelude::*;
|
||||
|
||||
use super::Pallet;
|
||||
|
||||
use snowbridge_core::ChannelId;
|
||||
pub use snowbridge_outbound_queue_merkle_tree::MerkleProof;
|
||||
|
||||
pub type ProcessMessageOriginOf<T> = <Pallet<T> as ProcessMessage>::Origin;
|
||||
|
||||
pub const LOG_TARGET: &str = "snowbridge-outbound-queue";
|
||||
|
||||
/// Message which has been assigned a nonce and will be committed at the end of a block
|
||||
#[derive(Encode, Decode, Clone, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct CommittedMessage {
|
||||
/// Message channel
|
||||
pub channel_id: ChannelId,
|
||||
/// Unique nonce to prevent replaying messages
|
||||
#[codec(compact)]
|
||||
pub nonce: u64,
|
||||
/// Command to execute in the Gateway contract
|
||||
pub command: u8,
|
||||
/// Params for the command
|
||||
pub params: Vec<u8>,
|
||||
/// Maximum gas allowed for message dispatch
|
||||
#[codec(compact)]
|
||||
pub max_dispatch_gas: u64,
|
||||
/// Maximum fee per gas
|
||||
#[codec(compact)]
|
||||
pub max_fee_per_gas: u128,
|
||||
/// Reward in ether for delivering this message, in addition to the gas refund
|
||||
#[codec(compact)]
|
||||
pub reward: u128,
|
||||
/// Message ID (Used for tracing messages across route, has no role in consensus)
|
||||
pub id: H256,
|
||||
}
|
||||
|
||||
/// Convert message into an ABI-encoded form for delivery to the InboundQueue contract on Ethereum
|
||||
impl From<CommittedMessage> for Token {
|
||||
fn from(x: CommittedMessage) -> Token {
|
||||
Token::Tuple(vec![
|
||||
Token::FixedBytes(Vec::from(x.channel_id.as_ref())),
|
||||
Token::Uint(x.nonce.into()),
|
||||
Token::Uint(x.command.into()),
|
||||
Token::Bytes(x.params.to_vec()),
|
||||
Token::Uint(x.max_dispatch_gas.into()),
|
||||
Token::Uint(x.max_fee_per_gas.into()),
|
||||
Token::Uint(x.reward.into()),
|
||||
Token::FixedBytes(Vec::from(x.id.as_ref())),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for fee calculations
|
||||
#[derive(
|
||||
Encode,
|
||||
Decode,
|
||||
Copy,
|
||||
Clone,
|
||||
PartialEq,
|
||||
RuntimeDebug,
|
||||
MaxEncodedLen,
|
||||
TypeInfo,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
pub struct FeeConfigRecord {
|
||||
/// ETH/DOT exchange rate
|
||||
pub exchange_rate: FixedU128,
|
||||
/// Ether fee per unit of gas
|
||||
pub fee_per_gas: u128,
|
||||
/// Ether reward for delivering message
|
||||
pub reward: u128,
|
||||
}
|
||||
|
||||
#[derive(RuntimeDebug)]
|
||||
pub struct InvalidFeeConfig;
|
||||
|
||||
impl FeeConfigRecord {
|
||||
pub fn validate(&self) -> Result<(), InvalidFeeConfig> {
|
||||
if self.exchange_rate == FixedU128::zero() {
|
||||
return Err(InvalidFeeConfig)
|
||||
}
|
||||
if self.fee_per_gas == 0 {
|
||||
return Err(InvalidFeeConfig)
|
||||
}
|
||||
if self.reward == 0 {
|
||||
return Err(InvalidFeeConfig)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
|
||||
//! Autogenerated weights for `snowbridge_outbound_queue`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
|
||||
//! DATE: 2023-10-19, STEPS: `2`, REPEAT: `1`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `192.168.1.7`, CPU: `<UNKNOWN>`
|
||||
//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("bridge-hub-rococo-dev")`, DB CACHE: `1024`
|
||||
|
||||
// Executed Command:
|
||||
// target/release/polkadot-parachain
|
||||
// benchmark
|
||||
// pallet
|
||||
// --chain=bridge-hub-rococo-dev
|
||||
// --pallet=snowbridge_outbound_queue
|
||||
// --extrinsic=*
|
||||
// --execution=wasm
|
||||
// --wasm-execution=compiled
|
||||
// --template
|
||||
// ../parachain/templates/module-weight-template.hbs
|
||||
// --output
|
||||
// ../parachain/pallets/outbound-queue/src/weights.rs
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for `snowbridge_outbound_queue`.
|
||||
pub trait WeightInfo {
|
||||
fn do_process_message() -> Weight;
|
||||
fn commit() -> Weight;
|
||||
fn commit_single() -> Weight;
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests.
|
||||
impl WeightInfo for () {
|
||||
/// Storage: EthereumOutboundQueue MessageLeaves (r:1 w:1)
|
||||
/// Proof Skipped: EthereumOutboundQueue MessageLeaves (max_values: Some(1), max_size: None, mode: Measured)
|
||||
/// Storage: EthereumOutboundQueue PendingHighPriorityMessageCount (r:1 w:1)
|
||||
/// Proof: EthereumOutboundQueue PendingHighPriorityMessageCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen)
|
||||
/// Storage: EthereumOutboundQueue Nonce (r:1 w:1)
|
||||
/// Proof: EthereumOutboundQueue Nonce (max_values: None, max_size: Some(20), added: 2495, mode: MaxEncodedLen)
|
||||
/// Storage: EthereumOutboundQueue Messages (r:1 w:1)
|
||||
/// Proof Skipped: EthereumOutboundQueue Messages (max_values: Some(1), max_size: None, mode: Measured)
|
||||
fn do_process_message() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `42`
|
||||
// Estimated: `3485`
|
||||
// Minimum execution time: 39_000_000 picoseconds.
|
||||
Weight::from_parts(39_000_000, 3485)
|
||||
.saturating_add(RocksDbWeight::get().reads(4_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(4_u64))
|
||||
}
|
||||
/// Storage: EthereumOutboundQueue MessageLeaves (r:1 w:0)
|
||||
/// Proof Skipped: EthereumOutboundQueue MessageLeaves (max_values: Some(1), max_size: None, mode: Measured)
|
||||
/// Storage: System Digest (r:1 w:1)
|
||||
/// Proof Skipped: System Digest (max_values: Some(1), max_size: None, mode: Measured)
|
||||
fn commit() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1094`
|
||||
// Estimated: `2579`
|
||||
// Minimum execution time: 28_000_000 picoseconds.
|
||||
Weight::from_parts(28_000_000, 2579)
|
||||
.saturating_add(RocksDbWeight::get().reads(2_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
|
||||
fn commit_single() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1094`
|
||||
// Estimated: `2579`
|
||||
// Minimum execution time: 9_000_000 picoseconds.
|
||||
Weight::from_parts(9_000_000, 1586)
|
||||
.saturating_add(RocksDbWeight::get().reads(2_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
[package]
|
||||
name = "snowbridge-system"
|
||||
description = "Snowbridge System"
|
||||
version = "0.1.1"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
edition = "2021"
|
||||
repository = "https://github.com/Snowfork/snowbridge"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = [
|
||||
"derive",
|
||||
] }
|
||||
scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
|
||||
frame-benchmarking = { path = "../../../../../substrate/frame/benchmarking", default-features = false, optional = true }
|
||||
frame-support = { path = "../../../../../substrate/frame/support", default-features = false }
|
||||
frame-system = { path = "../../../../../substrate/frame/system", default-features = false }
|
||||
log = { version = "0.4.20", default-features = false }
|
||||
|
||||
sp-core = { path = "../../../../../substrate/primitives/core", default-features = false }
|
||||
sp-std = { path = "../../../../../substrate/primitives/std", default-features = false }
|
||||
sp-io = { path = "../../../../../substrate/primitives/io", default-features = false }
|
||||
sp-runtime = { path = "../../../../../substrate/primitives/runtime", default-features = false }
|
||||
|
||||
xcm = { package = "staging-xcm", path = "../../../../../polkadot/xcm", default-features = false }
|
||||
xcm-builder = { package = "staging-xcm-builder", path = "../../../../../polkadot/xcm/xcm-builder", default-features = false }
|
||||
xcm-executor = { package = "staging-xcm-executor", path = "../../../../../polkadot/xcm/xcm-executor", default-features = false }
|
||||
|
||||
ethabi = { git = "https://github.com/Snowfork/ethabi-decode.git", package = "ethabi-decode", branch = "master", default-features = false }
|
||||
snowbridge-core = { path = "../../primitives/core", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = "0.4.1"
|
||||
hex-literal = { version = "0.4.1" }
|
||||
pallet-balances = { path = "../../../../../substrate/frame/balances" }
|
||||
sp-keyring = { path = "../../../../../substrate/primitives/keyring" }
|
||||
polkadot-primitives = { path = "../../../../../polkadot/primitives" }
|
||||
pallet-message-queue = { path = "../../../../../substrate/frame/message-queue" }
|
||||
snowbridge-outbound-queue = { path = "../outbound-queue" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"ethabi/std",
|
||||
"frame-benchmarking?/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"log/std",
|
||||
"scale-info/std",
|
||||
"snowbridge-core/std",
|
||||
"sp-core/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
"xcm-builder/std",
|
||||
"xcm-executor/std",
|
||||
"xcm/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"frame-benchmarking/runtime-benchmarks",
|
||||
"frame-support/runtime-benchmarks",
|
||||
"frame-system/runtime-benchmarks",
|
||||
"pallet-balances/runtime-benchmarks",
|
||||
"pallet-message-queue/runtime-benchmarks",
|
||||
"polkadot-primitives/runtime-benchmarks",
|
||||
"snowbridge-core/runtime-benchmarks",
|
||||
"snowbridge-outbound-queue/runtime-benchmarks",
|
||||
"sp-runtime/runtime-benchmarks",
|
||||
"xcm-builder/runtime-benchmarks",
|
||||
"xcm-executor/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"frame-support/try-runtime",
|
||||
"frame-system/try-runtime",
|
||||
"pallet-balances/try-runtime",
|
||||
"pallet-message-queue/try-runtime",
|
||||
"snowbridge-outbound-queue/try-runtime",
|
||||
"sp-runtime/try-runtime",
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
License: MIT-0
|
||||
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "snowbridge-system-runtime-api"
|
||||
description = "Snowbridge System Runtime API"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
repository = "https://github.com/Snowfork/snowbridge"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = [
|
||||
"derive",
|
||||
] }
|
||||
sp-core = { path = "../../../../../../substrate/primitives/core", default-features = false }
|
||||
sp-std = { path = "../../../../../../substrate/primitives/std", default-features = false }
|
||||
sp-api = { path = "../../../../../../substrate/primitives/api", default-features = false }
|
||||
xcm = { package = "staging-xcm", path = "../../../../../../polkadot/xcm", default-features = false }
|
||||
snowbridge-core = { path = "../../../primitives/core", default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"snowbridge-core/std",
|
||||
"sp-api/std",
|
||||
"sp-core/std",
|
||||
"sp-std/std",
|
||||
"xcm/std",
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use snowbridge_core::AgentId;
|
||||
use xcm::VersionedMultiLocation;
|
||||
|
||||
sp_api::decl_runtime_apis! {
|
||||
pub trait ControlApi
|
||||
{
|
||||
fn agent_id(location: VersionedMultiLocation) -> Option<AgentId>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Helpers for implementing runtime api
|
||||
|
||||
use snowbridge_core::AgentId;
|
||||
use xcm::{prelude::*, VersionedMultiLocation};
|
||||
|
||||
use crate::{agent_id_of, Config};
|
||||
|
||||
pub fn agent_id<Runtime>(location: VersionedMultiLocation) -> Option<AgentId>
|
||||
where
|
||||
Runtime: Config,
|
||||
{
|
||||
let location: MultiLocation = location.try_into().ok()?;
|
||||
agent_id_of::<Runtime>(&location).ok()
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Benchmarking setup for pallet-template
|
||||
use super::*;
|
||||
|
||||
#[allow(unused)]
|
||||
use crate::Pallet as SnowbridgeControl;
|
||||
use frame_benchmarking::v2::*;
|
||||
use frame_system::RawOrigin;
|
||||
use snowbridge_core::{eth, outbound::OperatingMode};
|
||||
use sp_runtime::SaturatedConversion;
|
||||
use xcm::prelude::*;
|
||||
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn fund_sovereign_account<T: Config>(para_id: ParaId) -> Result<(), BenchmarkError> {
|
||||
let amount: BalanceOf<T> = (10_000_000_000_000_u64).saturated_into::<u128>().saturated_into();
|
||||
let sovereign_account = sibling_sovereign_account::<T>(para_id);
|
||||
T::Token::mint_into(&sovereign_account, amount)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn upgrade() -> Result<(), BenchmarkError> {
|
||||
let impl_address = H160::repeat_byte(1);
|
||||
let impl_code_hash = H256::repeat_byte(1);
|
||||
|
||||
// Assume 256 bytes passed to initializer
|
||||
let params: Vec<u8> = (0..256).map(|_| 1u8).collect();
|
||||
|
||||
#[extrinsic_call]
|
||||
_(
|
||||
RawOrigin::Root,
|
||||
impl_address,
|
||||
impl_code_hash,
|
||||
Some(Initializer { params, maximum_required_gas: 100000 }),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn set_operating_mode() -> Result<(), BenchmarkError> {
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, OperatingMode::RejectingOutboundMessages);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn set_pricing_parameters() -> Result<(), BenchmarkError> {
|
||||
let params = T::DefaultPricingParameters::get();
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, params);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn create_agent() -> Result<(), BenchmarkError> {
|
||||
let origin_para_id = 2000;
|
||||
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
|
||||
let origin = T::Helper::make_xcm_origin(origin_location);
|
||||
fund_sovereign_account::<T>(origin_para_id.into())?;
|
||||
|
||||
#[extrinsic_call]
|
||||
_(origin as T::RuntimeOrigin);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn create_channel() -> Result<(), BenchmarkError> {
|
||||
let origin_para_id = 2000;
|
||||
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
|
||||
let origin = T::Helper::make_xcm_origin(origin_location);
|
||||
fund_sovereign_account::<T>(origin_para_id.into())?;
|
||||
|
||||
SnowbridgeControl::<T>::create_agent(origin.clone())?;
|
||||
|
||||
#[extrinsic_call]
|
||||
_(origin as T::RuntimeOrigin, OperatingMode::Normal);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn update_channel() -> Result<(), BenchmarkError> {
|
||||
let origin_para_id = 2000;
|
||||
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
|
||||
let origin = T::Helper::make_xcm_origin(origin_location);
|
||||
fund_sovereign_account::<T>(origin_para_id.into())?;
|
||||
SnowbridgeControl::<T>::create_agent(origin.clone())?;
|
||||
SnowbridgeControl::<T>::create_channel(origin.clone(), OperatingMode::Normal)?;
|
||||
|
||||
#[extrinsic_call]
|
||||
_(origin as T::RuntimeOrigin, OperatingMode::RejectingOutboundMessages);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn force_update_channel() -> Result<(), BenchmarkError> {
|
||||
let origin_para_id = 2000;
|
||||
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
|
||||
let origin = T::Helper::make_xcm_origin(origin_location);
|
||||
let channel_id: ChannelId = ParaId::from(origin_para_id).into();
|
||||
|
||||
fund_sovereign_account::<T>(origin_para_id.into())?;
|
||||
SnowbridgeControl::<T>::create_agent(origin.clone())?;
|
||||
SnowbridgeControl::<T>::create_channel(origin.clone(), OperatingMode::Normal)?;
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, channel_id, OperatingMode::RejectingOutboundMessages);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn transfer_native_from_agent() -> Result<(), BenchmarkError> {
|
||||
let origin_para_id = 2000;
|
||||
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
|
||||
let origin = T::Helper::make_xcm_origin(origin_location);
|
||||
fund_sovereign_account::<T>(origin_para_id.into())?;
|
||||
SnowbridgeControl::<T>::create_agent(origin.clone())?;
|
||||
SnowbridgeControl::<T>::create_channel(origin.clone(), OperatingMode::Normal)?;
|
||||
|
||||
#[extrinsic_call]
|
||||
_(origin as T::RuntimeOrigin, H160::default(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn force_transfer_native_from_agent() -> Result<(), BenchmarkError> {
|
||||
let origin_para_id = 2000;
|
||||
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
|
||||
let origin = T::Helper::make_xcm_origin(origin_location);
|
||||
fund_sovereign_account::<T>(origin_para_id.into())?;
|
||||
SnowbridgeControl::<T>::create_agent(origin.clone())?;
|
||||
|
||||
let versioned_location: VersionedMultiLocation = origin_location.into();
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, Box::new(versioned_location), H160::default(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn set_token_transfer_fees() -> Result<(), BenchmarkError> {
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, 1, 1, eth(1));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(
|
||||
SnowbridgeControl,
|
||||
crate::mock::new_test_ext(true),
|
||||
crate::mock::Test
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,681 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Governance API for controlling the Ethereum side of the bridge
|
||||
//!
|
||||
//! # Extrinsics
|
||||
//!
|
||||
//! ## Agents
|
||||
//!
|
||||
//! Agents are smart contracts on Ethereum that act as proxies for consensus systems on Polkadot
|
||||
//! networks.
|
||||
//!
|
||||
//! * [`Call::create_agent`]: Create agent for a sibling parachain
|
||||
//! * [`Call::transfer_native_from_agent`]: Withdraw ether from an agent
|
||||
//!
|
||||
//! The `create_agent` extrinsic should be called via an XCM `Transact` instruction from the sibling
|
||||
//! parachain.
|
||||
//!
|
||||
//! ## Channels
|
||||
//!
|
||||
//! Each sibling parachain has its own dedicated messaging channel for sending and receiving
|
||||
//! messages. As a prerequisite to creating a channel, the sibling should have already created
|
||||
//! an agent using the `create_agent` extrinsic.
|
||||
//!
|
||||
//! * [`Call::create_channel`]: Create channel for a sibling
|
||||
//! * [`Call::update_channel`]: Update a channel for a sibling
|
||||
//!
|
||||
//! ## Governance
|
||||
//!
|
||||
//! Only Polkadot governance itself can call these extrinsics. Delivery fees are waived.
|
||||
//!
|
||||
//! * [`Call::upgrade`]`: Upgrade the gateway contract
|
||||
//! * [`Call::set_operating_mode`]: Update the operating mode of the gateway contract
|
||||
//! * [`Call::force_update_channel`]: Allow root to update a channel for a sibling
|
||||
//! * [`Call::force_transfer_native_from_agent`]: Allow root to withdraw ether from an agent
|
||||
//!
|
||||
//! Typically, Polkadot governance will use the `force_transfer_native_from_agent` and
|
||||
//! `force_update_channel` and extrinsics to manage agents and channels for system parachains.
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
pub mod migration;
|
||||
|
||||
pub mod api;
|
||||
pub mod weights;
|
||||
pub use weights::*;
|
||||
|
||||
use frame_support::{
|
||||
pallet_prelude::*,
|
||||
traits::{
|
||||
fungible::{Inspect, Mutate},
|
||||
tokens::Preservation,
|
||||
Contains, EnsureOrigin,
|
||||
},
|
||||
};
|
||||
use frame_system::pallet_prelude::*;
|
||||
use snowbridge_core::{
|
||||
meth,
|
||||
outbound::{Command, Initializer, Message, OperatingMode, SendError, SendMessage},
|
||||
sibling_sovereign_account, AgentId, Channel, ChannelId, ParaId,
|
||||
PricingParameters as PricingParametersRecord, PRIMARY_GOVERNANCE_CHANNEL,
|
||||
SECONDARY_GOVERNANCE_CHANNEL,
|
||||
};
|
||||
use sp_core::{RuntimeDebug, H160, H256};
|
||||
use sp_io::hashing::blake2_256;
|
||||
use sp_runtime::{traits::BadOrigin, DispatchError, SaturatedConversion};
|
||||
use sp_std::prelude::*;
|
||||
use xcm::prelude::*;
|
||||
use xcm_executor::traits::ConvertLocation;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
use frame_support::traits::OriginTrait;
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
pub type BalanceOf<T> =
|
||||
<<T as pallet::Config>::Token as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
|
||||
pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
|
||||
pub type PricingParametersOf<T> = PricingParametersRecord<BalanceOf<T>>;
|
||||
|
||||
/// Ensure origin location is a sibling
|
||||
fn ensure_sibling<T>(location: &MultiLocation) -> Result<(ParaId, H256), DispatchError>
|
||||
where
|
||||
T: Config,
|
||||
{
|
||||
match location {
|
||||
MultiLocation { parents: 1, interior: X1(Parachain(para_id)) } => {
|
||||
let agent_id = agent_id_of::<T>(location)?;
|
||||
Ok(((*para_id).into(), agent_id))
|
||||
},
|
||||
_ => Err(BadOrigin.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Hash the location to produce an agent id
|
||||
fn agent_id_of<T: Config>(location: &MultiLocation) -> Result<H256, DispatchError> {
|
||||
T::AgentIdOf::convert_location(location).ok_or(Error::<T>::LocationConversionFailed.into())
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub trait BenchmarkHelper<O>
|
||||
where
|
||||
O: OriginTrait,
|
||||
{
|
||||
fn make_xcm_origin(location: MultiLocation) -> O;
|
||||
}
|
||||
|
||||
/// Whether a fee should be withdrawn to an account for sending an outbound message
|
||||
#[derive(Clone, PartialEq, RuntimeDebug)]
|
||||
pub enum PaysFee<T>
|
||||
where
|
||||
T: Config,
|
||||
{
|
||||
/// Fully charge includes (local + remote fee)
|
||||
Yes(AccountIdOf<T>),
|
||||
/// Partially charge includes local fee only
|
||||
Partial(AccountIdOf<T>),
|
||||
/// No charge
|
||||
No,
|
||||
}
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use snowbridge_core::StaticLookup;
|
||||
use sp_core::U256;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config {
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||||
|
||||
/// Send messages to Ethereum
|
||||
type OutboundQueue: SendMessage<Balance = BalanceOf<Self>>;
|
||||
|
||||
/// Origin check for XCM locations that can create agents
|
||||
type SiblingOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = MultiLocation>;
|
||||
|
||||
/// Converts MultiLocation to AgentId
|
||||
type AgentIdOf: ConvertLocation<AgentId>;
|
||||
|
||||
/// Token reserved for control operations
|
||||
type Token: Mutate<Self::AccountId>;
|
||||
|
||||
/// TreasuryAccount to collect fees
|
||||
#[pallet::constant]
|
||||
type TreasuryAccount: Get<Self::AccountId>;
|
||||
|
||||
/// Number of decimal places of local currency
|
||||
type DefaultPricingParameters: Get<PricingParametersOf<Self>>;
|
||||
|
||||
/// Cost of delivering a message from Ethereum
|
||||
type InboundDeliveryCost: Get<BalanceOf<Self>>;
|
||||
|
||||
type WeightInfo: WeightInfo;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type Helper: BenchmarkHelper<Self::RuntimeOrigin>;
|
||||
}
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// An Upgrade message was sent to the Gateway
|
||||
Upgrade {
|
||||
impl_address: H160,
|
||||
impl_code_hash: H256,
|
||||
initializer_params_hash: Option<H256>,
|
||||
},
|
||||
/// An CreateAgent message was sent to the Gateway
|
||||
CreateAgent {
|
||||
location: Box<MultiLocation>,
|
||||
agent_id: AgentId,
|
||||
},
|
||||
/// An CreateChannel message was sent to the Gateway
|
||||
CreateChannel {
|
||||
channel_id: ChannelId,
|
||||
agent_id: AgentId,
|
||||
},
|
||||
/// An UpdateChannel message was sent to the Gateway
|
||||
UpdateChannel {
|
||||
channel_id: ChannelId,
|
||||
mode: OperatingMode,
|
||||
},
|
||||
/// An SetOperatingMode message was sent to the Gateway
|
||||
SetOperatingMode {
|
||||
mode: OperatingMode,
|
||||
},
|
||||
/// An TransferNativeFromAgent message was sent to the Gateway
|
||||
TransferNativeFromAgent {
|
||||
agent_id: AgentId,
|
||||
recipient: H160,
|
||||
amount: u128,
|
||||
},
|
||||
/// A SetTokenTransferFees message was sent to the Gateway
|
||||
SetTokenTransferFees {
|
||||
create_asset_xcm: u128,
|
||||
transfer_asset_xcm: u128,
|
||||
register_token: U256,
|
||||
},
|
||||
PricingParametersChanged {
|
||||
params: PricingParametersOf<T>,
|
||||
},
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
LocationConversionFailed,
|
||||
AgentAlreadyCreated,
|
||||
NoAgent,
|
||||
ChannelAlreadyCreated,
|
||||
NoChannel,
|
||||
UnsupportedLocationVersion,
|
||||
InvalidLocation,
|
||||
Send(SendError),
|
||||
InvalidTokenTransferFees,
|
||||
InvalidPricingParameters,
|
||||
}
|
||||
|
||||
/// The set of registered agents
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn agents)]
|
||||
pub type Agents<T: Config> = StorageMap<_, Twox64Concat, AgentId, (), OptionQuery>;
|
||||
|
||||
/// The set of registered channels
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn channels)]
|
||||
pub type Channels<T: Config> = StorageMap<_, Twox64Concat, ChannelId, Channel, OptionQuery>;
|
||||
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn parameters)]
|
||||
pub type PricingParameters<T: Config> =
|
||||
StorageValue<_, PricingParametersOf<T>, ValueQuery, T::DefaultPricingParameters>;
|
||||
|
||||
#[pallet::genesis_config]
|
||||
#[derive(frame_support::DefaultNoBound)]
|
||||
pub struct GenesisConfig<T: Config> {
|
||||
// Own parachain id
|
||||
pub para_id: ParaId,
|
||||
// AssetHub's parachain id
|
||||
pub asset_hub_para_id: ParaId,
|
||||
#[serde(skip)]
|
||||
pub _config: PhantomData<T>,
|
||||
}
|
||||
|
||||
#[pallet::genesis_build]
|
||||
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
|
||||
fn build(&self) {
|
||||
Pallet::<T>::initialize(self.para_id, self.asset_hub_para_id).expect("infallible; qed");
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Sends command to the Gateway contract to upgrade itself with a new implementation
|
||||
/// contract
|
||||
///
|
||||
/// Fee required: No
|
||||
///
|
||||
/// - `origin`: Must be `Root`.
|
||||
/// - `impl_address`: The address of the implementation contract.
|
||||
/// - `impl_code_hash`: The codehash of the implementation contract.
|
||||
/// - `initializer`: Optionally call an initializer on the implementation contract.
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight((T::WeightInfo::upgrade(), DispatchClass::Operational))]
|
||||
pub fn upgrade(
|
||||
origin: OriginFor<T>,
|
||||
impl_address: H160,
|
||||
impl_code_hash: H256,
|
||||
initializer: Option<Initializer>,
|
||||
) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
|
||||
let initializer_params_hash: Option<H256> =
|
||||
initializer.as_ref().map(|i| H256::from(blake2_256(i.params.as_ref())));
|
||||
let command = Command::Upgrade { impl_address, impl_code_hash, initializer };
|
||||
Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::<T>::No)?;
|
||||
|
||||
Self::deposit_event(Event::<T>::Upgrade {
|
||||
impl_address,
|
||||
impl_code_hash,
|
||||
initializer_params_hash,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a message to the Gateway contract to change its operating mode
|
||||
///
|
||||
/// Fee required: No
|
||||
///
|
||||
/// - `origin`: Must be `MultiLocation`
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight((T::WeightInfo::set_operating_mode(), DispatchClass::Operational))]
|
||||
pub fn set_operating_mode(origin: OriginFor<T>, mode: OperatingMode) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
|
||||
let command = Command::SetOperatingMode { mode };
|
||||
Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::<T>::No)?;
|
||||
|
||||
Self::deposit_event(Event::<T>::SetOperatingMode { mode });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set pricing parameters on both sides of the bridge
|
||||
///
|
||||
/// Fee required: No
|
||||
///
|
||||
/// - `origin`: Must be root
|
||||
#[pallet::call_index(2)]
|
||||
#[pallet::weight((T::WeightInfo::set_pricing_parameters(), DispatchClass::Operational))]
|
||||
pub fn set_pricing_parameters(
|
||||
origin: OriginFor<T>,
|
||||
params: PricingParametersOf<T>,
|
||||
) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
params.validate().map_err(|_| Error::<T>::InvalidPricingParameters)?;
|
||||
PricingParameters::<T>::put(params.clone());
|
||||
|
||||
let command = Command::SetPricingParameters {
|
||||
exchange_rate: params.exchange_rate.into(),
|
||||
delivery_cost: T::InboundDeliveryCost::get().saturated_into::<u128>(),
|
||||
};
|
||||
Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::<T>::No)?;
|
||||
|
||||
Self::deposit_event(Event::PricingParametersChanged { params });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a command to the Gateway contract to instantiate a new agent contract representing
|
||||
/// `origin`.
|
||||
///
|
||||
/// Fee required: Yes
|
||||
///
|
||||
/// - `origin`: Must be `MultiLocation` of a sibling parachain
|
||||
#[pallet::call_index(3)]
|
||||
#[pallet::weight(T::WeightInfo::create_agent())]
|
||||
pub fn create_agent(origin: OriginFor<T>) -> DispatchResult {
|
||||
let origin_location: MultiLocation = T::SiblingOrigin::ensure_origin(origin)?;
|
||||
|
||||
// Ensure that origin location is some consensus system on a sibling parachain
|
||||
let (para_id, agent_id) = ensure_sibling::<T>(&origin_location)?;
|
||||
|
||||
// Record the agent id or fail if it has already been created
|
||||
ensure!(!Agents::<T>::contains_key(agent_id), Error::<T>::AgentAlreadyCreated);
|
||||
Agents::<T>::insert(agent_id, ());
|
||||
|
||||
let command = Command::CreateAgent { agent_id };
|
||||
let pays_fee = PaysFee::<T>::Yes(sibling_sovereign_account::<T>(para_id));
|
||||
Self::send(SECONDARY_GOVERNANCE_CHANNEL, command, pays_fee)?;
|
||||
|
||||
Self::deposit_event(Event::<T>::CreateAgent {
|
||||
location: Box::new(origin_location),
|
||||
agent_id,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a message to the Gateway contract to create a new channel representing `origin`
|
||||
///
|
||||
/// Fee required: Yes
|
||||
///
|
||||
/// This extrinsic is permissionless, so a fee is charged to prevent spamming and pay
|
||||
/// for execution costs on the remote side.
|
||||
///
|
||||
/// The message is sent over the bridge on BridgeHub's own channel to the Gateway.
|
||||
///
|
||||
/// - `origin`: Must be `MultiLocation`
|
||||
/// - `mode`: Initial operating mode of the channel
|
||||
#[pallet::call_index(4)]
|
||||
#[pallet::weight(T::WeightInfo::create_channel())]
|
||||
pub fn create_channel(origin: OriginFor<T>, mode: OperatingMode) -> DispatchResult {
|
||||
let origin_location: MultiLocation = T::SiblingOrigin::ensure_origin(origin)?;
|
||||
|
||||
// Ensure that origin location is a sibling parachain
|
||||
let (para_id, agent_id) = ensure_sibling::<T>(&origin_location)?;
|
||||
|
||||
let channel_id: ChannelId = para_id.into();
|
||||
|
||||
ensure!(Agents::<T>::contains_key(agent_id), Error::<T>::NoAgent);
|
||||
ensure!(!Channels::<T>::contains_key(channel_id), Error::<T>::ChannelAlreadyCreated);
|
||||
|
||||
let channel = Channel { agent_id, para_id };
|
||||
Channels::<T>::insert(channel_id, channel);
|
||||
|
||||
let command = Command::CreateChannel { channel_id, agent_id, mode };
|
||||
let pays_fee = PaysFee::<T>::Yes(sibling_sovereign_account::<T>(para_id));
|
||||
Self::send(SECONDARY_GOVERNANCE_CHANNEL, command, pays_fee)?;
|
||||
|
||||
Self::deposit_event(Event::<T>::CreateChannel { channel_id, agent_id });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a message to the Gateway contract to update a channel configuration
|
||||
///
|
||||
/// The origin must already have a channel initialized, as this message is sent over it.
|
||||
///
|
||||
/// A partial fee will be charged for local processing only.
|
||||
///
|
||||
/// - `origin`: Must be `MultiLocation`
|
||||
/// - `mode`: Initial operating mode of the channel
|
||||
#[pallet::call_index(5)]
|
||||
#[pallet::weight(T::WeightInfo::update_channel())]
|
||||
pub fn update_channel(origin: OriginFor<T>, mode: OperatingMode) -> DispatchResult {
|
||||
let origin_location: MultiLocation = T::SiblingOrigin::ensure_origin(origin)?;
|
||||
|
||||
// Ensure that origin location is a sibling parachain
|
||||
let (para_id, _) = ensure_sibling::<T>(&origin_location)?;
|
||||
|
||||
let channel_id: ChannelId = para_id.into();
|
||||
|
||||
ensure!(Channels::<T>::contains_key(channel_id), Error::<T>::NoChannel);
|
||||
|
||||
let command = Command::UpdateChannel { channel_id, mode };
|
||||
let pays_fee = PaysFee::<T>::Partial(sibling_sovereign_account::<T>(para_id));
|
||||
|
||||
// Parachains send the update message on their own channel
|
||||
Self::send(channel_id, command, pays_fee)?;
|
||||
|
||||
Self::deposit_event(Event::<T>::UpdateChannel { channel_id, mode });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a message to the Gateway contract to update an arbitrary channel
|
||||
///
|
||||
/// Fee required: No
|
||||
///
|
||||
/// - `origin`: Must be root
|
||||
/// - `channel_id`: ID of channel
|
||||
/// - `mode`: Initial operating mode of the channel
|
||||
/// - `outbound_fee`: Fee charged to users for sending outbound messages to Polkadot
|
||||
#[pallet::call_index(6)]
|
||||
#[pallet::weight(T::WeightInfo::force_update_channel())]
|
||||
pub fn force_update_channel(
|
||||
origin: OriginFor<T>,
|
||||
channel_id: ChannelId,
|
||||
mode: OperatingMode,
|
||||
) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
|
||||
ensure!(Channels::<T>::contains_key(channel_id), Error::<T>::NoChannel);
|
||||
|
||||
let command = Command::UpdateChannel { channel_id, mode };
|
||||
Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::<T>::No)?;
|
||||
|
||||
Self::deposit_event(Event::<T>::UpdateChannel { channel_id, mode });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a message to the Gateway contract to transfer ether from an agent to `recipient`.
|
||||
///
|
||||
/// A partial fee will be charged for local processing only.
|
||||
///
|
||||
/// - `origin`: Must be `MultiLocation`
|
||||
#[pallet::call_index(7)]
|
||||
#[pallet::weight(T::WeightInfo::transfer_native_from_agent())]
|
||||
pub fn transfer_native_from_agent(
|
||||
origin: OriginFor<T>,
|
||||
recipient: H160,
|
||||
amount: u128,
|
||||
) -> DispatchResult {
|
||||
let origin_location: MultiLocation = T::SiblingOrigin::ensure_origin(origin)?;
|
||||
|
||||
// Ensure that origin location is some consensus system on a sibling parachain
|
||||
let (para_id, agent_id) = ensure_sibling::<T>(&origin_location)?;
|
||||
|
||||
// Since the origin is also the owner of the channel, they only need to pay
|
||||
// the local processing fee.
|
||||
let pays_fee = PaysFee::<T>::Partial(sibling_sovereign_account::<T>(para_id));
|
||||
|
||||
Self::do_transfer_native_from_agent(
|
||||
agent_id,
|
||||
para_id.into(),
|
||||
recipient,
|
||||
amount,
|
||||
pays_fee,
|
||||
)
|
||||
}
|
||||
|
||||
/// Sends a message to the Gateway contract to transfer ether from an agent to `recipient`.
|
||||
///
|
||||
/// Privileged. Can only be called by root.
|
||||
///
|
||||
/// Fee required: No
|
||||
///
|
||||
/// - `origin`: Must be root
|
||||
/// - `location`: Location used to resolve the agent
|
||||
/// - `recipient`: Recipient of funds
|
||||
/// - `amount`: Amount to transfer
|
||||
#[pallet::call_index(8)]
|
||||
#[pallet::weight(T::WeightInfo::force_transfer_native_from_agent())]
|
||||
pub fn force_transfer_native_from_agent(
|
||||
origin: OriginFor<T>,
|
||||
location: Box<VersionedMultiLocation>,
|
||||
recipient: H160,
|
||||
amount: u128,
|
||||
) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
|
||||
// Ensure that location is some consensus system on a sibling parachain
|
||||
let location: MultiLocation =
|
||||
(*location).try_into().map_err(|_| Error::<T>::UnsupportedLocationVersion)?;
|
||||
let (_, agent_id) =
|
||||
ensure_sibling::<T>(&location).map_err(|_| Error::<T>::InvalidLocation)?;
|
||||
|
||||
let pays_fee = PaysFee::<T>::No;
|
||||
|
||||
Self::do_transfer_native_from_agent(
|
||||
agent_id,
|
||||
PRIMARY_GOVERNANCE_CHANNEL,
|
||||
recipient,
|
||||
amount,
|
||||
pays_fee,
|
||||
)
|
||||
}
|
||||
|
||||
/// Sends a message to the Gateway contract to update fee related parameters for
|
||||
/// token transfers.
|
||||
///
|
||||
/// Privileged. Can only be called by root.
|
||||
///
|
||||
/// Fee required: No
|
||||
///
|
||||
/// - `origin`: Must be root
|
||||
/// - `create_asset_xcm`: The XCM execution cost for creating a new asset class on AssetHub,
|
||||
/// in DOT
|
||||
/// - `transfer_asset_xcm`: The XCM execution cost for performing a reserve transfer on
|
||||
/// AssetHub, in DOT
|
||||
/// - `register_token`: The Ether fee for registering a new token, to discourage spamming
|
||||
#[pallet::call_index(9)]
|
||||
#[pallet::weight((T::WeightInfo::set_token_transfer_fees(), DispatchClass::Operational))]
|
||||
pub fn set_token_transfer_fees(
|
||||
origin: OriginFor<T>,
|
||||
create_asset_xcm: u128,
|
||||
transfer_asset_xcm: u128,
|
||||
register_token: U256,
|
||||
) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
|
||||
// Basic validation of new costs. Particularly for token registration, we want to ensure
|
||||
// its relatively expensive to discourage spamming. Like at least 100 USD.
|
||||
ensure!(
|
||||
create_asset_xcm > 0 && transfer_asset_xcm > 0 && register_token > meth(100),
|
||||
Error::<T>::InvalidTokenTransferFees
|
||||
);
|
||||
|
||||
let command = Command::SetTokenTransferFees {
|
||||
create_asset_xcm,
|
||||
transfer_asset_xcm,
|
||||
register_token,
|
||||
};
|
||||
Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::<T>::No)?;
|
||||
|
||||
Self::deposit_event(Event::<T>::SetTokenTransferFees {
|
||||
create_asset_xcm,
|
||||
transfer_asset_xcm,
|
||||
register_token,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Send `command` to the Gateway on the Channel identified by `channel_id`
|
||||
fn send(channel_id: ChannelId, command: Command, pays_fee: PaysFee<T>) -> DispatchResult {
|
||||
let message = Message { id: None, channel_id, command };
|
||||
let (ticket, fee) =
|
||||
T::OutboundQueue::validate(&message).map_err(|err| Error::<T>::Send(err))?;
|
||||
|
||||
let payment = match pays_fee {
|
||||
PaysFee::Yes(account) => Some((account, fee.total())),
|
||||
PaysFee::Partial(account) => Some((account, fee.local)),
|
||||
PaysFee::No => None,
|
||||
};
|
||||
|
||||
if let Some((payer, fee)) = payment {
|
||||
T::Token::transfer(
|
||||
&payer,
|
||||
&T::TreasuryAccount::get(),
|
||||
fee,
|
||||
Preservation::Preserve,
|
||||
)?;
|
||||
}
|
||||
|
||||
T::OutboundQueue::deliver(ticket).map_err(|err| Error::<T>::Send(err))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Issue a `Command::TransferNativeFromAgent` command. The command will be sent on the
|
||||
/// channel `channel_id`
|
||||
pub fn do_transfer_native_from_agent(
|
||||
agent_id: H256,
|
||||
channel_id: ChannelId,
|
||||
recipient: H160,
|
||||
amount: u128,
|
||||
pays_fee: PaysFee<T>,
|
||||
) -> DispatchResult {
|
||||
ensure!(Agents::<T>::contains_key(agent_id), Error::<T>::NoAgent);
|
||||
|
||||
let command = Command::TransferNativeFromAgent { agent_id, recipient, amount };
|
||||
Self::send(channel_id, command, pays_fee)?;
|
||||
|
||||
Self::deposit_event(Event::<T>::TransferNativeFromAgent {
|
||||
agent_id,
|
||||
recipient,
|
||||
amount,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initializes agents and channels.
|
||||
pub fn initialize(para_id: ParaId, asset_hub_para_id: ParaId) -> Result<(), DispatchError> {
|
||||
// Asset Hub
|
||||
let asset_hub_location: MultiLocation =
|
||||
ParentThen(X1(Parachain(asset_hub_para_id.into()))).into();
|
||||
let asset_hub_agent_id = agent_id_of::<T>(&asset_hub_location)?;
|
||||
let asset_hub_channel_id: ChannelId = asset_hub_para_id.into();
|
||||
Agents::<T>::insert(asset_hub_agent_id, ());
|
||||
Channels::<T>::insert(
|
||||
asset_hub_channel_id,
|
||||
Channel { agent_id: asset_hub_agent_id, para_id: asset_hub_para_id },
|
||||
);
|
||||
|
||||
// Governance channels
|
||||
let bridge_hub_agent_id = agent_id_of::<T>(&MultiLocation::here())?;
|
||||
// Agent for BridgeHub
|
||||
Agents::<T>::insert(bridge_hub_agent_id, ());
|
||||
|
||||
// Primary governance channel
|
||||
Channels::<T>::insert(
|
||||
PRIMARY_GOVERNANCE_CHANNEL,
|
||||
Channel { agent_id: bridge_hub_agent_id, para_id },
|
||||
);
|
||||
|
||||
// Secondary governance channel
|
||||
Channels::<T>::insert(
|
||||
SECONDARY_GOVERNANCE_CHANNEL,
|
||||
Channel { agent_id: bridge_hub_agent_id, para_id },
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if the pallet has been initialized.
|
||||
pub(crate) fn is_initialized() -> bool {
|
||||
let primary_exists = Channels::<T>::contains_key(PRIMARY_GOVERNANCE_CHANNEL);
|
||||
let secondary_exists = Channels::<T>::contains_key(SECONDARY_GOVERNANCE_CHANNEL);
|
||||
primary_exists && secondary_exists
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> StaticLookup for Pallet<T> {
|
||||
type Source = ChannelId;
|
||||
type Target = Channel;
|
||||
fn lookup(channel_id: Self::Source) -> Option<Self::Target> {
|
||||
Channels::<T>::get(channel_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Contains<ChannelId> for Pallet<T> {
|
||||
fn contains(channel_id: &ChannelId) -> bool {
|
||||
Channels::<T>::get(channel_id).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Get<PricingParametersOf<T>> for Pallet<T> {
|
||||
fn get() -> PricingParametersOf<T> {
|
||||
PricingParameters::<T>::get()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Governance API for controlling the Ethereum side of the bridge
|
||||
use super::*;
|
||||
use frame_support::traits::OnRuntimeUpgrade;
|
||||
use log;
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use sp_runtime::TryRuntimeError;
|
||||
|
||||
pub mod v0 {
|
||||
use frame_support::{pallet_prelude::*, weights::Weight};
|
||||
|
||||
use super::*;
|
||||
|
||||
const LOG_TARGET: &str = "ethereum_system::migration";
|
||||
|
||||
pub struct InitializeOnUpgrade<T, BridgeHubParaId, AssetHubParaId>(
|
||||
sp_std::marker::PhantomData<(T, BridgeHubParaId, AssetHubParaId)>,
|
||||
);
|
||||
impl<T, BridgeHubParaId, AssetHubParaId> OnRuntimeUpgrade
|
||||
for InitializeOnUpgrade<T, BridgeHubParaId, AssetHubParaId>
|
||||
where
|
||||
T: Config,
|
||||
BridgeHubParaId: Get<u32>,
|
||||
AssetHubParaId: Get<u32>,
|
||||
{
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
if !Pallet::<T>::is_initialized() {
|
||||
Pallet::<T>::initialize(
|
||||
BridgeHubParaId::get().into(),
|
||||
AssetHubParaId::get().into(),
|
||||
)
|
||||
.expect("infallible; qed");
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"Ethereum system initialized."
|
||||
);
|
||||
T::DbWeight::get().reads_writes(2, 5)
|
||||
} else {
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"Ethereum system already initialized. Skipping."
|
||||
);
|
||||
T::DbWeight::get().reads(2)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
|
||||
if !Pallet::<T>::is_initialized() {
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"Agents and channels not initialized. Initialization will run."
|
||||
);
|
||||
} else {
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"Agents and channels are initialized. Initialization will not run."
|
||||
);
|
||||
}
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
frame_support::ensure!(
|
||||
Pallet::<T>::is_initialized(),
|
||||
"Agents and channels were not initialized."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use crate as snowbridge_system;
|
||||
use frame_support::{
|
||||
parameter_types,
|
||||
traits::{tokens::fungible::Mutate, ConstU128, ConstU16, ConstU64, ConstU8},
|
||||
weights::IdentityFee,
|
||||
PalletId,
|
||||
};
|
||||
use sp_core::H256;
|
||||
use xcm_executor::traits::ConvertLocation;
|
||||
|
||||
use snowbridge_core::{
|
||||
gwei, meth, outbound::ConstantGasMeter, sibling_sovereign_account, AgentId, AllowSiblingsOnly,
|
||||
ParaId, PricingParameters, Rewards,
|
||||
};
|
||||
use sp_runtime::{
|
||||
traits::{AccountIdConversion, BlakeTwo256, IdentityLookup, Keccak256},
|
||||
AccountId32, BuildStorage, FixedU128,
|
||||
};
|
||||
use xcm::prelude::*;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
use crate::BenchmarkHelper;
|
||||
|
||||
type Block = frame_system::mocking::MockBlock<Test>;
|
||||
type Balance = u128;
|
||||
|
||||
pub type AccountId = AccountId32;
|
||||
|
||||
// A stripped-down version of pallet-xcm that only inserts an XCM origin into the runtime
|
||||
#[allow(dead_code)]
|
||||
#[frame_support::pallet]
|
||||
mod pallet_xcm_origin {
|
||||
use frame_support::{
|
||||
pallet_prelude::*,
|
||||
traits::{Contains, OriginTrait},
|
||||
};
|
||||
use xcm::latest::prelude::*;
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config {
|
||||
type RuntimeOrigin: From<Origin> + From<<Self as frame_system::Config>::RuntimeOrigin>;
|
||||
}
|
||||
|
||||
// Insert this custom Origin into the aggregate RuntimeOrigin
|
||||
#[pallet::origin]
|
||||
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
pub struct Origin(pub MultiLocation);
|
||||
|
||||
impl From<MultiLocation> for Origin {
|
||||
fn from(location: MultiLocation) -> Origin {
|
||||
Origin(location)
|
||||
}
|
||||
}
|
||||
|
||||
/// `EnsureOrigin` implementation succeeding with a `MultiLocation` value to recognize and
|
||||
/// filter the contained location
|
||||
pub struct EnsureXcm<F>(PhantomData<F>);
|
||||
impl<O: OriginTrait + From<Origin>, F: Contains<MultiLocation>> EnsureOrigin<O> for EnsureXcm<F>
|
||||
where
|
||||
O::PalletsOrigin: From<Origin> + TryInto<Origin, Error = O::PalletsOrigin>,
|
||||
{
|
||||
type Success = MultiLocation;
|
||||
|
||||
fn try_origin(outer: O) -> Result<Self::Success, O> {
|
||||
outer.try_with_caller(|caller| {
|
||||
caller.try_into().and_then(|o| match o {
|
||||
Origin(location) if F::contains(&location) => Ok(location),
|
||||
o => Err(o.into()),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn try_successful_origin() -> Result<O, ()> {
|
||||
Ok(O::from(Origin(MultiLocation { parents: 1, interior: X1(Parachain(2000)) })))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure a mock runtime to test the pallet.
|
||||
frame_support::construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: frame_system,
|
||||
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
|
||||
XcmOrigin: pallet_xcm_origin::{Pallet, Origin},
|
||||
OutboundQueue: snowbridge_outbound_queue::{Pallet, Call, Storage, Event<T>},
|
||||
EthereumSystem: snowbridge_system,
|
||||
MessageQueue: pallet_message_queue::{Pallet, Call, Storage, Event<T>}
|
||||
}
|
||||
);
|
||||
|
||||
impl frame_system::Config for Test {
|
||||
type BaseCallFilter = frame_support::traits::Everything;
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type RuntimeTask = RuntimeTask;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = ConstU64<250>;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = pallet_balances::AccountData<u128>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ConstU16<42>;
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = frame_support::traits::ConstU32<16>;
|
||||
type Nonce = u64;
|
||||
type Block = Block;
|
||||
}
|
||||
|
||||
impl pallet_balances::Config for Test {
|
||||
type MaxLocks = ();
|
||||
type MaxReserves = ();
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
type Balance = Balance;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type DustRemoval = ();
|
||||
type ExistentialDeposit = ConstU128<1>;
|
||||
type AccountStore = System;
|
||||
type WeightInfo = ();
|
||||
type FreezeIdentifier = ();
|
||||
type MaxFreezes = ();
|
||||
type RuntimeHoldReason = ();
|
||||
type RuntimeFreezeReason = ();
|
||||
type MaxHolds = ();
|
||||
}
|
||||
|
||||
impl pallet_xcm_origin::Config for Test {
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const HeapSize: u32 = 32 * 1024;
|
||||
pub const MaxStale: u32 = 32;
|
||||
pub static ServiceWeight: Option<Weight> = Some(Weight::from_parts(100, 100));
|
||||
}
|
||||
|
||||
impl pallet_message_queue::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type WeightInfo = ();
|
||||
type MessageProcessor = OutboundQueue;
|
||||
type Size = u32;
|
||||
type QueueChangeHandler = ();
|
||||
type HeapSize = HeapSize;
|
||||
type MaxStale = MaxStale;
|
||||
type ServiceWeight = ServiceWeight;
|
||||
type QueuePausedQuery = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const MaxMessagePayloadSize: u32 = 1024;
|
||||
pub const MaxMessagesPerBlock: u32 = 20;
|
||||
pub const OwnParaId: ParaId = ParaId::new(1013);
|
||||
}
|
||||
|
||||
impl snowbridge_outbound_queue::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Hashing = Keccak256;
|
||||
type MessageQueue = MessageQueue;
|
||||
type Decimals = ConstU8<10>;
|
||||
type MaxMessagePayloadSize = MaxMessagePayloadSize;
|
||||
type MaxMessagesPerBlock = MaxMessagesPerBlock;
|
||||
type GasMeter = ConstantGasMeter;
|
||||
type Balance = u128;
|
||||
type PricingParameters = EthereumSystem;
|
||||
type Channels = EthereumSystem;
|
||||
type WeightToFee = IdentityFee<u128>;
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const SS58Prefix: u8 = 42;
|
||||
pub const AnyNetwork: Option<NetworkId> = None;
|
||||
pub const RelayNetwork: Option<NetworkId> = Some(NetworkId::Kusama);
|
||||
pub const RelayLocation: MultiLocation = MultiLocation::parent();
|
||||
pub UniversalLocation: InteriorMultiLocation =
|
||||
X2(GlobalConsensus(RelayNetwork::get().unwrap()), Parachain(1013));
|
||||
}
|
||||
|
||||
pub const DOT: u128 = 10_000_000_000;
|
||||
|
||||
parameter_types! {
|
||||
pub TreasuryAccount: AccountId = PalletId(*b"py/trsry").into_account_truncating();
|
||||
pub Fee: u64 = 1000;
|
||||
pub const RococoNetwork: NetworkId = NetworkId::Rococo;
|
||||
pub const InitialFunding: u128 = 1_000_000_000_000;
|
||||
pub AssetHubParaId: ParaId = ParaId::new(1000);
|
||||
pub TestParaId: u32 = 2000;
|
||||
pub Parameters: PricingParameters<u128> = PricingParameters {
|
||||
exchange_rate: FixedU128::from_rational(1, 400),
|
||||
fee_per_gas: gwei(20),
|
||||
rewards: Rewards { local: DOT, remote: meth(1) }
|
||||
};
|
||||
pub const InboundDeliveryCost: u128 = 1_000_000_000;
|
||||
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
impl BenchmarkHelper<RuntimeOrigin> for () {
|
||||
fn make_xcm_origin(location: MultiLocation) -> RuntimeOrigin {
|
||||
RuntimeOrigin::from(pallet_xcm_origin::Origin(location))
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type OutboundQueue = OutboundQueue;
|
||||
type SiblingOrigin = pallet_xcm_origin::EnsureXcm<AllowSiblingsOnly>;
|
||||
type AgentIdOf = snowbridge_core::AgentIdOf;
|
||||
type TreasuryAccount = TreasuryAccount;
|
||||
type Token = Balances;
|
||||
type DefaultPricingParameters = Parameters;
|
||||
type WeightInfo = ();
|
||||
type InboundDeliveryCost = InboundDeliveryCost;
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type Helper = ();
|
||||
}
|
||||
|
||||
// Build genesis storage according to the mock runtime.
|
||||
pub fn new_test_ext(genesis_build: bool) -> sp_io::TestExternalities {
|
||||
let mut storage = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
|
||||
if genesis_build {
|
||||
crate::GenesisConfig::<Test> {
|
||||
para_id: OwnParaId::get(),
|
||||
asset_hub_para_id: AssetHubParaId::get(),
|
||||
_config: Default::default(),
|
||||
}
|
||||
.assimilate_storage(&mut storage)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let mut ext: sp_io::TestExternalities = storage.into();
|
||||
let initial_amount = InitialFunding::get();
|
||||
let test_para_id = TestParaId::get();
|
||||
let sovereign_account = sibling_sovereign_account::<Test>(test_para_id.into());
|
||||
let treasury_account = TreasuryAccount::get();
|
||||
ext.execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
Balances::mint_into(&AccountId32::from([0; 32]), initial_amount).unwrap();
|
||||
Balances::mint_into(&sovereign_account, initial_amount).unwrap();
|
||||
Balances::mint_into(&treasury_account, initial_amount).unwrap();
|
||||
});
|
||||
ext
|
||||
}
|
||||
|
||||
// Test helpers
|
||||
|
||||
pub fn make_xcm_origin(location: MultiLocation) -> RuntimeOrigin {
|
||||
pallet_xcm_origin::Origin(location).into()
|
||||
}
|
||||
|
||||
pub fn make_agent_id(location: MultiLocation) -> AgentId {
|
||||
<Test as snowbridge_system::Config>::AgentIdOf::convert_location(&location)
|
||||
.expect("convert location")
|
||||
}
|
||||
@@ -0,0 +1,664 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use crate::{mock::*, *};
|
||||
use frame_support::{assert_noop, assert_ok};
|
||||
use hex_literal::hex;
|
||||
use snowbridge_core::{eth, sibling_sovereign_account_raw};
|
||||
use sp_core::H256;
|
||||
use sp_runtime::{AccountId32, DispatchError::BadOrigin, TokenError};
|
||||
|
||||
#[test]
|
||||
fn create_agent() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin_para_id = 2000;
|
||||
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
|
||||
let agent_id = make_agent_id(origin_location);
|
||||
let sovereign_account = sibling_sovereign_account::<Test>(origin_para_id.into());
|
||||
|
||||
// fund sovereign account of origin
|
||||
let _ = Balances::mint_into(&sovereign_account, 10000);
|
||||
|
||||
assert!(!Agents::<Test>::contains_key(agent_id));
|
||||
|
||||
let origin = make_xcm_origin(origin_location);
|
||||
assert_ok!(EthereumSystem::create_agent(origin));
|
||||
|
||||
assert!(Agents::<Test>::contains_key(agent_id));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_agent_for_here() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin_location = MultiLocation::here();
|
||||
let agent_id = make_agent_id(origin_location);
|
||||
assert_eq!(
|
||||
agent_id,
|
||||
hex!("03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314").into(),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_agent_fails_on_funds_unavailable() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(2000)) };
|
||||
let origin = make_xcm_origin(origin_location);
|
||||
// Reset balance of sovereign_account to zero so to trigger the FundsUnavailable error
|
||||
let sovereign_account = sibling_sovereign_account::<Test>(2000.into());
|
||||
Balances::set_balance(&sovereign_account, 0);
|
||||
assert_noop!(EthereumSystem::create_agent(origin), TokenError::FundsUnavailable);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_agent_bad_origin() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
// relay chain location not allowed
|
||||
assert_noop!(
|
||||
EthereumSystem::create_agent(make_xcm_origin(MultiLocation {
|
||||
parents: 1,
|
||||
interior: Here,
|
||||
})),
|
||||
BadOrigin,
|
||||
);
|
||||
|
||||
// local account location not allowed
|
||||
assert_noop!(
|
||||
EthereumSystem::create_agent(make_xcm_origin(MultiLocation {
|
||||
parents: 0,
|
||||
interior: X1(Junction::AccountId32 { network: None, id: [67u8; 32] }),
|
||||
})),
|
||||
BadOrigin,
|
||||
);
|
||||
|
||||
// Signed origin not allowed
|
||||
assert_noop!(
|
||||
EthereumSystem::create_agent(RuntimeOrigin::signed([14; 32].into())),
|
||||
BadOrigin
|
||||
);
|
||||
|
||||
// None origin not allowed
|
||||
assert_noop!(EthereumSystem::create_agent(RuntimeOrigin::none()), BadOrigin);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upgrade_as_root() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin = RuntimeOrigin::root();
|
||||
let address: H160 = Default::default();
|
||||
let code_hash: H256 = Default::default();
|
||||
|
||||
assert_ok!(EthereumSystem::upgrade(origin, address, code_hash, None));
|
||||
|
||||
System::assert_last_event(RuntimeEvent::EthereumSystem(crate::Event::Upgrade {
|
||||
impl_address: address,
|
||||
impl_code_hash: code_hash,
|
||||
initializer_params_hash: None,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upgrade_as_signed_fails() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin = RuntimeOrigin::signed(AccountId32::new([0; 32]));
|
||||
let address: H160 = Default::default();
|
||||
let code_hash: H256 = Default::default();
|
||||
|
||||
assert_noop!(EthereumSystem::upgrade(origin, address, code_hash, None), BadOrigin);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upgrade_with_params() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin = RuntimeOrigin::root();
|
||||
let address: H160 = Default::default();
|
||||
let code_hash: H256 = Default::default();
|
||||
let initializer: Option<Initializer> =
|
||||
Some(Initializer { params: [0; 256].into(), maximum_required_gas: 10000 });
|
||||
assert_ok!(EthereumSystem::upgrade(origin, address, code_hash, initializer));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_operating_mode() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin = RuntimeOrigin::root();
|
||||
let mode = OperatingMode::RejectingOutboundMessages;
|
||||
|
||||
assert_ok!(EthereumSystem::set_operating_mode(origin, mode));
|
||||
|
||||
System::assert_last_event(RuntimeEvent::EthereumSystem(crate::Event::SetOperatingMode {
|
||||
mode,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_operating_mode_as_signed_fails() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin = RuntimeOrigin::signed([14; 32].into());
|
||||
let mode = OperatingMode::RejectingOutboundMessages;
|
||||
|
||||
assert_noop!(EthereumSystem::set_operating_mode(origin, mode), BadOrigin);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_pricing_parameters() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin = RuntimeOrigin::root();
|
||||
let mut params = Parameters::get();
|
||||
params.rewards.local = 7;
|
||||
|
||||
assert_ok!(EthereumSystem::set_pricing_parameters(origin, params));
|
||||
|
||||
assert_eq!(PricingParameters::<Test>::get().rewards.local, 7);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_pricing_parameters_as_signed_fails() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin = RuntimeOrigin::signed([14; 32].into());
|
||||
let params = Parameters::get();
|
||||
|
||||
assert_noop!(EthereumSystem::set_pricing_parameters(origin, params), BadOrigin);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_pricing_parameters_invalid() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin = RuntimeOrigin::root();
|
||||
let mut params = Parameters::get();
|
||||
params.rewards.local = 0;
|
||||
|
||||
assert_noop!(
|
||||
EthereumSystem::set_pricing_parameters(origin.clone(), params),
|
||||
Error::<Test>::InvalidPricingParameters
|
||||
);
|
||||
|
||||
let mut params = Parameters::get();
|
||||
params.exchange_rate = 0u128.into();
|
||||
assert_noop!(
|
||||
EthereumSystem::set_pricing_parameters(origin.clone(), params),
|
||||
Error::<Test>::InvalidPricingParameters
|
||||
);
|
||||
params = Parameters::get();
|
||||
params.fee_per_gas = sp_core::U256::zero();
|
||||
assert_noop!(
|
||||
EthereumSystem::set_pricing_parameters(origin.clone(), params),
|
||||
Error::<Test>::InvalidPricingParameters
|
||||
);
|
||||
params = Parameters::get();
|
||||
params.rewards.local = 0;
|
||||
assert_noop!(
|
||||
EthereumSystem::set_pricing_parameters(origin.clone(), params),
|
||||
Error::<Test>::InvalidPricingParameters
|
||||
);
|
||||
params = Parameters::get();
|
||||
params.rewards.remote = sp_core::U256::zero();
|
||||
assert_noop!(
|
||||
EthereumSystem::set_pricing_parameters(origin, params),
|
||||
Error::<Test>::InvalidPricingParameters
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_token_transfer_fees() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin = RuntimeOrigin::root();
|
||||
|
||||
assert_ok!(EthereumSystem::set_token_transfer_fees(origin, 1, 1, eth(1)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_token_transfer_fees_root_only() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin = RuntimeOrigin::signed([14; 32].into());
|
||||
|
||||
assert_noop!(EthereumSystem::set_token_transfer_fees(origin, 1, 1, 1.into()), BadOrigin);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_token_transfer_fees_invalid() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin = RuntimeOrigin::root();
|
||||
|
||||
assert_noop!(
|
||||
EthereumSystem::set_token_transfer_fees(origin, 0, 0, 0.into()),
|
||||
Error::<Test>::InvalidTokenTransferFees
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_channel() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin_para_id = 2000;
|
||||
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
|
||||
let sovereign_account = sibling_sovereign_account::<Test>(origin_para_id.into());
|
||||
let origin = make_xcm_origin(origin_location);
|
||||
|
||||
// fund sovereign account of origin
|
||||
let _ = Balances::mint_into(&sovereign_account, 10000);
|
||||
|
||||
assert_ok!(EthereumSystem::create_agent(origin.clone()));
|
||||
assert_ok!(EthereumSystem::create_channel(origin, OperatingMode::Normal));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_channel_fail_already_exists() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin_para_id = 2000;
|
||||
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
|
||||
let sovereign_account = sibling_sovereign_account::<Test>(origin_para_id.into());
|
||||
let origin = make_xcm_origin(origin_location);
|
||||
|
||||
// fund sovereign account of origin
|
||||
let _ = Balances::mint_into(&sovereign_account, 10000);
|
||||
|
||||
assert_ok!(EthereumSystem::create_agent(origin.clone()));
|
||||
assert_ok!(EthereumSystem::create_channel(origin.clone(), OperatingMode::Normal));
|
||||
|
||||
assert_noop!(
|
||||
EthereumSystem::create_channel(origin, OperatingMode::Normal),
|
||||
Error::<Test>::ChannelAlreadyCreated
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_channel_bad_origin() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
// relay chain location not allowed
|
||||
assert_noop!(
|
||||
EthereumSystem::create_channel(
|
||||
make_xcm_origin(MultiLocation { parents: 1, interior: Here }),
|
||||
OperatingMode::Normal,
|
||||
),
|
||||
BadOrigin,
|
||||
);
|
||||
|
||||
// child of sibling location not allowed
|
||||
assert_noop!(
|
||||
EthereumSystem::create_channel(
|
||||
make_xcm_origin(MultiLocation {
|
||||
parents: 1,
|
||||
interior: X2(
|
||||
Parachain(2000),
|
||||
Junction::AccountId32 { network: None, id: [67u8; 32] }
|
||||
),
|
||||
}),
|
||||
OperatingMode::Normal,
|
||||
),
|
||||
BadOrigin,
|
||||
);
|
||||
|
||||
// local account location not allowed
|
||||
assert_noop!(
|
||||
EthereumSystem::create_channel(
|
||||
make_xcm_origin(MultiLocation {
|
||||
parents: 0,
|
||||
interior: X1(Junction::AccountId32 { network: None, id: [67u8; 32] }),
|
||||
}),
|
||||
OperatingMode::Normal,
|
||||
),
|
||||
BadOrigin,
|
||||
);
|
||||
|
||||
// Signed origin not allowed
|
||||
assert_noop!(
|
||||
EthereumSystem::create_channel(
|
||||
RuntimeOrigin::signed([14; 32].into()),
|
||||
OperatingMode::Normal,
|
||||
),
|
||||
BadOrigin
|
||||
);
|
||||
|
||||
// None origin not allowed
|
||||
assert_noop!(EthereumSystem::create_agent(RuntimeOrigin::none()), BadOrigin);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_channel() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin_para_id = 2000;
|
||||
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
|
||||
let sovereign_account = sibling_sovereign_account::<Test>(origin_para_id.into());
|
||||
let origin = make_xcm_origin(origin_location);
|
||||
|
||||
// First create the channel
|
||||
let _ = Balances::mint_into(&sovereign_account, 10000);
|
||||
assert_ok!(EthereumSystem::create_agent(origin.clone()));
|
||||
assert_ok!(EthereumSystem::create_channel(origin.clone(), OperatingMode::Normal));
|
||||
|
||||
// Now try to update it
|
||||
assert_ok!(EthereumSystem::update_channel(origin, OperatingMode::Normal));
|
||||
|
||||
System::assert_last_event(RuntimeEvent::EthereumSystem(crate::Event::UpdateChannel {
|
||||
channel_id: ParaId::from(2000).into(),
|
||||
mode: OperatingMode::Normal,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_channel_bad_origin() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let mode = OperatingMode::Normal;
|
||||
|
||||
// relay chain location not allowed
|
||||
assert_noop!(
|
||||
EthereumSystem::update_channel(
|
||||
make_xcm_origin(MultiLocation { parents: 1, interior: Here }),
|
||||
mode,
|
||||
),
|
||||
BadOrigin,
|
||||
);
|
||||
|
||||
// child of sibling location not allowed
|
||||
assert_noop!(
|
||||
EthereumSystem::update_channel(
|
||||
make_xcm_origin(MultiLocation {
|
||||
parents: 1,
|
||||
interior: X2(
|
||||
Parachain(2000),
|
||||
Junction::AccountId32 { network: None, id: [67u8; 32] }
|
||||
),
|
||||
}),
|
||||
mode,
|
||||
),
|
||||
BadOrigin,
|
||||
);
|
||||
|
||||
// local account location not allowed
|
||||
assert_noop!(
|
||||
EthereumSystem::update_channel(
|
||||
make_xcm_origin(MultiLocation {
|
||||
parents: 0,
|
||||
interior: X1(Junction::AccountId32 { network: None, id: [67u8; 32] }),
|
||||
}),
|
||||
mode,
|
||||
),
|
||||
BadOrigin,
|
||||
);
|
||||
|
||||
// Signed origin not allowed
|
||||
assert_noop!(
|
||||
EthereumSystem::update_channel(RuntimeOrigin::signed([14; 32].into()), mode),
|
||||
BadOrigin
|
||||
);
|
||||
|
||||
// None origin not allowed
|
||||
assert_noop!(EthereumSystem::update_channel(RuntimeOrigin::none(), mode), BadOrigin);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_channel_fails_not_exist() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(2000)) };
|
||||
let origin = make_xcm_origin(origin_location);
|
||||
|
||||
// Now try to update it
|
||||
assert_noop!(
|
||||
EthereumSystem::update_channel(origin, OperatingMode::Normal),
|
||||
Error::<Test>::NoChannel
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_update_channel() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin_para_id = 2000;
|
||||
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
|
||||
let sovereign_account = sibling_sovereign_account::<Test>(origin_para_id.into());
|
||||
let origin = make_xcm_origin(origin_location);
|
||||
|
||||
let channel_id: ChannelId = ParaId::from(origin_para_id).into();
|
||||
|
||||
// First create the channel
|
||||
let _ = Balances::mint_into(&sovereign_account, 10000);
|
||||
assert_ok!(EthereumSystem::create_agent(origin.clone()));
|
||||
assert_ok!(EthereumSystem::create_channel(origin.clone(), OperatingMode::Normal));
|
||||
|
||||
// Now try to force update it
|
||||
let force_origin = RuntimeOrigin::root();
|
||||
assert_ok!(EthereumSystem::force_update_channel(
|
||||
force_origin,
|
||||
channel_id,
|
||||
OperatingMode::Normal,
|
||||
));
|
||||
|
||||
System::assert_last_event(RuntimeEvent::EthereumSystem(crate::Event::UpdateChannel {
|
||||
channel_id: ParaId::from(2000).into(),
|
||||
mode: OperatingMode::Normal,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_update_channel_bad_origin() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let mode = OperatingMode::Normal;
|
||||
|
||||
// signed origin not allowed
|
||||
assert_noop!(
|
||||
EthereumSystem::force_update_channel(
|
||||
RuntimeOrigin::signed([14; 32].into()),
|
||||
ParaId::from(1000).into(),
|
||||
mode,
|
||||
),
|
||||
BadOrigin,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_native_from_agent() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(2000)) };
|
||||
let origin = make_xcm_origin(origin_location);
|
||||
let recipient: H160 = [27u8; 20].into();
|
||||
let amount = 103435;
|
||||
|
||||
// First create the agent and channel
|
||||
assert_ok!(EthereumSystem::create_agent(origin.clone()));
|
||||
assert_ok!(EthereumSystem::create_channel(origin, OperatingMode::Normal));
|
||||
|
||||
let origin = make_xcm_origin(origin_location);
|
||||
assert_ok!(EthereumSystem::transfer_native_from_agent(origin, recipient, amount),);
|
||||
|
||||
System::assert_last_event(RuntimeEvent::EthereumSystem(
|
||||
crate::Event::TransferNativeFromAgent {
|
||||
agent_id: make_agent_id(origin_location),
|
||||
recipient,
|
||||
amount,
|
||||
},
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_transfer_native_from_agent() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let origin = RuntimeOrigin::root();
|
||||
let location = MultiLocation { parents: 1, interior: X1(Parachain(2000)) };
|
||||
let versioned_location: Box<VersionedMultiLocation> = Box::new(location.into());
|
||||
let recipient: H160 = [27u8; 20].into();
|
||||
let amount = 103435;
|
||||
|
||||
// First create the agent
|
||||
Agents::<Test>::insert(make_agent_id(location), ());
|
||||
|
||||
assert_ok!(EthereumSystem::force_transfer_native_from_agent(
|
||||
origin,
|
||||
versioned_location,
|
||||
recipient,
|
||||
amount
|
||||
),);
|
||||
|
||||
System::assert_last_event(RuntimeEvent::EthereumSystem(
|
||||
crate::Event::TransferNativeFromAgent {
|
||||
agent_id: make_agent_id(location),
|
||||
recipient,
|
||||
amount,
|
||||
},
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_transfer_native_from_agent_bad_origin() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let recipient: H160 = [27u8; 20].into();
|
||||
let amount = 103435;
|
||||
|
||||
// signed origin not allowed
|
||||
assert_noop!(
|
||||
EthereumSystem::force_transfer_native_from_agent(
|
||||
RuntimeOrigin::signed([14; 32].into()),
|
||||
Box::new(
|
||||
MultiLocation {
|
||||
parents: 1,
|
||||
interior: X2(
|
||||
Parachain(2000),
|
||||
Junction::AccountId32 { network: None, id: [67u8; 32] }
|
||||
),
|
||||
}
|
||||
.into()
|
||||
),
|
||||
recipient,
|
||||
amount,
|
||||
),
|
||||
BadOrigin,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE: The following tests are not actually tests and are more about obtaining location
|
||||
// conversions for devops purposes. They need to be removed here and incorporated into a command
|
||||
// line utility.
|
||||
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn check_sibling_sovereign_account() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let para_id = 1001;
|
||||
let sovereign_account = sibling_sovereign_account::<Test>(para_id.into());
|
||||
let sovereign_account_raw = sibling_sovereign_account_raw(para_id.into());
|
||||
println!(
|
||||
"Sovereign account for parachain {}: {:#?}",
|
||||
para_id,
|
||||
hex::encode(sovereign_account.clone())
|
||||
);
|
||||
assert_eq!(sovereign_account, sovereign_account_raw.into());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn charge_fee_for_create_agent() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let para_id: u32 = TestParaId::get();
|
||||
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(para_id)) };
|
||||
let origin = make_xcm_origin(origin_location);
|
||||
let sovereign_account = sibling_sovereign_account::<Test>(para_id.into());
|
||||
let (_, agent_id) = ensure_sibling::<Test>(&origin_location).unwrap();
|
||||
|
||||
let initial_sovereign_balance = Balances::balance(&sovereign_account);
|
||||
assert_ok!(EthereumSystem::create_agent(origin.clone()));
|
||||
let fee_charged = initial_sovereign_balance - Balances::balance(&sovereign_account);
|
||||
|
||||
assert_ok!(EthereumSystem::create_channel(origin, OperatingMode::Normal));
|
||||
|
||||
// assert sovereign_balance decreased by (fee.base_fee + fee.delivery_fee)
|
||||
let message = Message {
|
||||
id: None,
|
||||
channel_id: ParaId::from(para_id).into(),
|
||||
command: Command::CreateAgent { agent_id },
|
||||
};
|
||||
let (_, fee) = OutboundQueue::validate(&message).unwrap();
|
||||
assert_eq!(fee.local + fee.remote, fee_charged);
|
||||
|
||||
// and treasury_balance increased
|
||||
let treasury_balance = Balances::balance(&TreasuryAccount::get());
|
||||
assert!(treasury_balance > InitialFunding::get());
|
||||
|
||||
let final_sovereign_balance = Balances::balance(&sovereign_account);
|
||||
// (sovereign_balance + treasury_balance) keeps the same
|
||||
assert_eq!(final_sovereign_balance + treasury_balance, { InitialFunding::get() * 2 });
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn charge_fee_for_transfer_native_from_agent() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let para_id: u32 = TestParaId::get();
|
||||
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(para_id)) };
|
||||
let recipient: H160 = [27u8; 20].into();
|
||||
let amount = 103435;
|
||||
let origin = make_xcm_origin(origin_location);
|
||||
let (_, agent_id) = ensure_sibling::<Test>(&origin_location).unwrap();
|
||||
|
||||
let sovereign_account = sibling_sovereign_account::<Test>(para_id.into());
|
||||
|
||||
// create_agent & create_channel first
|
||||
assert_ok!(EthereumSystem::create_agent(origin.clone()));
|
||||
assert_ok!(EthereumSystem::create_channel(origin.clone(), OperatingMode::Normal));
|
||||
|
||||
// assert sovereign_balance decreased by only the base_fee
|
||||
let sovereign_balance_before = Balances::balance(&sovereign_account);
|
||||
assert_ok!(EthereumSystem::transfer_native_from_agent(origin.clone(), recipient, amount));
|
||||
let message = Message {
|
||||
id: None,
|
||||
channel_id: ParaId::from(para_id).into(),
|
||||
command: Command::TransferNativeFromAgent { agent_id, recipient, amount },
|
||||
};
|
||||
let (_, fee) = OutboundQueue::validate(&message).unwrap();
|
||||
let sovereign_balance_after = Balances::balance(&sovereign_account);
|
||||
assert_eq!(sovereign_balance_after + fee.local, sovereign_balance_before);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn charge_fee_for_upgrade() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
let para_id: u32 = TestParaId::get();
|
||||
let origin = RuntimeOrigin::root();
|
||||
let address: H160 = Default::default();
|
||||
let code_hash: H256 = Default::default();
|
||||
let initializer: Option<Initializer> =
|
||||
Some(Initializer { params: [0; 256].into(), maximum_required_gas: 10000 });
|
||||
assert_ok!(EthereumSystem::upgrade(origin, address, code_hash, initializer.clone()));
|
||||
|
||||
// assert sovereign_balance does not change as we do not charge for sudo operations
|
||||
let sovereign_account = sibling_sovereign_account::<Test>(para_id.into());
|
||||
let sovereign_balance = Balances::balance(&sovereign_account);
|
||||
assert_eq!(sovereign_balance, InitialFunding::get());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn genesis_build_initializes_correctly() {
|
||||
new_test_ext(true).execute_with(|| {
|
||||
assert!(EthereumSystem::is_initialized(), "Ethereum uninitialized.");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_genesis_build_is_uninitialized() {
|
||||
new_test_ext(false).execute_with(|| {
|
||||
assert!(!EthereumSystem::is_initialized(), "Ethereum initialized.");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
|
||||
//! Autogenerated weights for `snowbridge_system`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
|
||||
//! DATE: 2023-10-09, STEPS: `2`, REPEAT: `1`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `crake.local`, CPU: `<UNKNOWN>`
|
||||
//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("bridge-hub-rococo-dev")`, DB CACHE: `1024`
|
||||
|
||||
// Executed Command:
|
||||
// target/release/polkadot-parachain
|
||||
// benchmark
|
||||
// pallet
|
||||
// --chain
|
||||
// bridge-hub-rococo-dev
|
||||
// --pallet=snowbridge_system
|
||||
// --extrinsic=*
|
||||
// --execution=wasm
|
||||
// --wasm-execution=compiled
|
||||
// --template
|
||||
// ../parachain/templates/module-weight-template.hbs
|
||||
// --output
|
||||
// ../parachain/pallets/control/src/weights.rs
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for `snowbridge_system`.
|
||||
pub trait WeightInfo {
|
||||
fn upgrade() -> Weight;
|
||||
fn create_agent() -> Weight;
|
||||
fn create_channel() -> Weight;
|
||||
fn update_channel() -> Weight;
|
||||
fn force_update_channel() -> Weight;
|
||||
fn set_operating_mode() -> Weight;
|
||||
fn transfer_native_from_agent() -> Weight;
|
||||
fn force_transfer_native_from_agent() -> Weight;
|
||||
fn set_token_transfer_fees() -> Weight;
|
||||
fn set_pricing_parameters() -> Weight;
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests.
|
||||
impl WeightInfo for () {
|
||||
/// Storage: ParachainInfo ParachainId (r:1 w:0)
|
||||
/// Proof: ParachainInfo ParachainId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen)
|
||||
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
|
||||
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue BookStateFor (r:1 w:1)
|
||||
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue ServiceHead (r:1 w:1)
|
||||
/// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue Pages (r:0 w:1)
|
||||
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
|
||||
fn upgrade() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `80`
|
||||
// Estimated: `3517`
|
||||
// Minimum execution time: 44_000_000 picoseconds.
|
||||
Weight::from_parts(44_000_000, 3517)
|
||||
.saturating_add(RocksDbWeight::get().reads(4_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: EthereumSystem Agents (r:1 w:1)
|
||||
/// Proof: EthereumSystem Agents (max_values: None, max_size: Some(40), added: 2515, mode: MaxEncodedLen)
|
||||
/// Storage: System Account (r:2 w:2)
|
||||
/// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen)
|
||||
/// Storage: ParachainInfo ParachainId (r:1 w:0)
|
||||
/// Proof: ParachainInfo ParachainId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen)
|
||||
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
|
||||
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue BookStateFor (r:1 w:1)
|
||||
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue ServiceHead (r:1 w:1)
|
||||
/// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue Pages (r:0 w:1)
|
||||
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
|
||||
fn create_agent() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `187`
|
||||
// Estimated: `6196`
|
||||
// Minimum execution time: 85_000_000 picoseconds.
|
||||
Weight::from_parts(85_000_000, 6196)
|
||||
.saturating_add(RocksDbWeight::get().reads(7_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(6_u64))
|
||||
}
|
||||
/// Storage: System Account (r:2 w:2)
|
||||
/// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen)
|
||||
/// Storage: EthereumSystem Agents (r:1 w:0)
|
||||
/// Proof: EthereumSystem Agents (max_values: None, max_size: Some(40), added: 2515, mode: MaxEncodedLen)
|
||||
/// Storage: EthereumSystem Channels (r:1 w:1)
|
||||
/// Proof: EthereumSystem Channels (max_values: None, max_size: Some(12), added: 2487, mode: MaxEncodedLen)
|
||||
/// Storage: ParachainInfo ParachainId (r:1 w:0)
|
||||
/// Proof: ParachainInfo ParachainId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen)
|
||||
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
|
||||
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue BookStateFor (r:1 w:1)
|
||||
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue Pages (r:1 w:1)
|
||||
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
|
||||
fn create_channel() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `602`
|
||||
// Estimated: `69050`
|
||||
// Minimum execution time: 83_000_000 picoseconds.
|
||||
Weight::from_parts(83_000_000, 69050)
|
||||
.saturating_add(RocksDbWeight::get().reads(8_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(5_u64))
|
||||
}
|
||||
/// Storage: EthereumSystem Channels (r:1 w:0)
|
||||
/// Proof: EthereumSystem Channels (max_values: None, max_size: Some(12), added: 2487, mode: MaxEncodedLen)
|
||||
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
|
||||
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue BookStateFor (r:2 w:2)
|
||||
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue ServiceHead (r:1 w:0)
|
||||
/// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue Pages (r:0 w:1)
|
||||
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
|
||||
fn update_channel() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `256`
|
||||
// Estimated: `6044`
|
||||
// Minimum execution time: 40_000_000 picoseconds.
|
||||
Weight::from_parts(40_000_000, 6044)
|
||||
.saturating_add(RocksDbWeight::get().reads(5_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: EthereumSystem Channels (r:1 w:0)
|
||||
/// Proof: EthereumSystem Channels (max_values: None, max_size: Some(12), added: 2487, mode: MaxEncodedLen)
|
||||
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
|
||||
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue BookStateFor (r:2 w:2)
|
||||
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue ServiceHead (r:1 w:0)
|
||||
/// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue Pages (r:0 w:1)
|
||||
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
|
||||
fn force_update_channel() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `256`
|
||||
// Estimated: `6044`
|
||||
// Minimum execution time: 41_000_000 picoseconds.
|
||||
Weight::from_parts(41_000_000, 6044)
|
||||
.saturating_add(RocksDbWeight::get().reads(5_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: ParachainInfo ParachainId (r:1 w:0)
|
||||
/// Proof: ParachainInfo ParachainId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen)
|
||||
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
|
||||
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue BookStateFor (r:1 w:1)
|
||||
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue ServiceHead (r:1 w:1)
|
||||
/// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue Pages (r:0 w:1)
|
||||
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
|
||||
fn set_operating_mode() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `80`
|
||||
// Estimated: `3517`
|
||||
// Minimum execution time: 31_000_000 picoseconds.
|
||||
Weight::from_parts(31_000_000, 3517)
|
||||
.saturating_add(RocksDbWeight::get().reads(4_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: EthereumSystem Agents (r:1 w:0)
|
||||
/// Proof: EthereumSystem Agents (max_values: None, max_size: Some(40), added: 2515, mode: MaxEncodedLen)
|
||||
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
|
||||
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue BookStateFor (r:2 w:2)
|
||||
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue ServiceHead (r:1 w:0)
|
||||
/// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue Pages (r:0 w:1)
|
||||
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
|
||||
fn transfer_native_from_agent() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `252`
|
||||
// Estimated: `6044`
|
||||
// Minimum execution time: 45_000_000 picoseconds.
|
||||
Weight::from_parts(45_000_000, 6044)
|
||||
.saturating_add(RocksDbWeight::get().reads(5_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: EthereumSystem Agents (r:1 w:0)
|
||||
/// Proof: EthereumSystem Agents (max_values: None, max_size: Some(40), added: 2515, mode: MaxEncodedLen)
|
||||
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
|
||||
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue BookStateFor (r:2 w:2)
|
||||
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue ServiceHead (r:1 w:0)
|
||||
/// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue Pages (r:0 w:1)
|
||||
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
|
||||
fn force_transfer_native_from_agent() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `252`
|
||||
// Estimated: `6044`
|
||||
// Minimum execution time: 42_000_000 picoseconds.
|
||||
Weight::from_parts(42_000_000, 6044)
|
||||
.saturating_add(RocksDbWeight::get().reads(5_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
|
||||
/// Storage: ParachainInfo ParachainId (r:1 w:0)
|
||||
/// Proof: ParachainInfo ParachainId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen)
|
||||
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
|
||||
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue BookStateFor (r:1 w:1)
|
||||
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue ServiceHead (r:1 w:1)
|
||||
/// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue Pages (r:0 w:1)
|
||||
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
|
||||
fn set_token_transfer_fees() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `80`
|
||||
// Estimated: `3517`
|
||||
// Minimum execution time: 31_000_000 picoseconds.
|
||||
Weight::from_parts(42_000_000, 3517)
|
||||
.saturating_add(RocksDbWeight::get().reads(4_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
|
||||
/// Storage: ParachainInfo ParachainId (r:1 w:0)
|
||||
/// Proof: ParachainInfo ParachainId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen)
|
||||
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
|
||||
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue BookStateFor (r:1 w:1)
|
||||
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue ServiceHead (r:1 w:1)
|
||||
/// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen)
|
||||
/// Storage: MessageQueue Pages (r:0 w:1)
|
||||
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
|
||||
fn set_pricing_parameters() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `80`
|
||||
// Estimated: `3517`
|
||||
// Minimum execution time: 31_000_000 picoseconds.
|
||||
Weight::from_parts(42_000_000, 3517)
|
||||
.saturating_add(RocksDbWeight::get().reads(4_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
[package]
|
||||
name = "snowbridge-beacon-primitives"
|
||||
description = "Snowbridge Beacon Primitives"
|
||||
version = "0.0.1"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.188", optional = true, features = ["derive"] }
|
||||
hex = { version = "0.4", default-features = false }
|
||||
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false }
|
||||
scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
|
||||
rlp = { version = "0.5", default-features = false }
|
||||
|
||||
frame-support = { path = "../../../../../substrate/frame/support", default-features = false }
|
||||
frame-system = { path = "../../../../../substrate/frame/system", default-features = false }
|
||||
sp-runtime = { path = "../../../../../substrate/primitives/runtime", default-features = false }
|
||||
sp-core = { path = "../../../../../substrate/primitives/core", default-features = false }
|
||||
sp-std = { path = "../../../../../substrate/primitives/std", default-features = false }
|
||||
sp-io = { path = "../../../../../substrate/primitives/io", default-features = false }
|
||||
|
||||
ssz_rs = { version = "0.9.0", default-features = false }
|
||||
ssz_rs_derive = { version = "0.9.0", default-features = false }
|
||||
byte-slice-cast = { version = "1.2.1", default-features = false }
|
||||
|
||||
snowbridge-ethereum = { path = "../../primitives/ethereum", default-features = false }
|
||||
static_assertions = { version = "1.1.0" }
|
||||
milagro_bls = { git = "https://github.com/snowfork/milagro_bls", default-features = false, rev = "a6d66e4eb89015e352fb1c9f7b661ecdbb5b2176" }
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = { version = "0.4.1" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"byte-slice-cast/std",
|
||||
"codec/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"hex/std",
|
||||
"milagro_bls/std",
|
||||
"rlp/std",
|
||||
"scale-info/std",
|
||||
"serde",
|
||||
"snowbridge-ethereum/std",
|
||||
"sp-core/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
"ssz_rs/std",
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use sp_std::{convert::TryInto, prelude::*};
|
||||
use ssz_rs::{Bitvector, Deserialize};
|
||||
|
||||
pub fn decompress_sync_committee_bits<
|
||||
const SYNC_COMMITTEE_SIZE: usize,
|
||||
const SYNC_COMMITTEE_BITS_SIZE: usize,
|
||||
>(
|
||||
input: [u8; SYNC_COMMITTEE_BITS_SIZE],
|
||||
) -> [u8; SYNC_COMMITTEE_SIZE] {
|
||||
Bitvector::<{ SYNC_COMMITTEE_SIZE }>::deserialize(&input)
|
||||
.expect("checked statically; qed")
|
||||
.iter()
|
||||
.map(|bit| u8::from(bit == true))
|
||||
.collect::<Vec<u8>>()
|
||||
.try_into()
|
||||
.expect("checked statically; qed")
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use crate::{PublicKey, Signature};
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::{ensure, PalletError};
|
||||
pub use milagro_bls::{
|
||||
AggregatePublicKey, AggregateSignature, PublicKey as PublicKeyPrepared,
|
||||
Signature as SignaturePrepared,
|
||||
};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::H256;
|
||||
use sp_runtime::RuntimeDebug;
|
||||
use sp_std::prelude::*;
|
||||
|
||||
#[derive(Copy, Clone, Encode, Decode, Eq, PartialEq, TypeInfo, RuntimeDebug, PalletError)]
|
||||
pub enum BlsError {
|
||||
InvalidSignature,
|
||||
InvalidPublicKey,
|
||||
InvalidAggregatePublicKeys,
|
||||
SignatureVerificationFailed,
|
||||
}
|
||||
|
||||
/// fast_aggregate_verify optimized with aggregate key subtracting absent ones.
|
||||
pub fn fast_aggregate_verify(
|
||||
aggregate_pubkey: &PublicKeyPrepared,
|
||||
absent_pubkeys: &Vec<PublicKeyPrepared>,
|
||||
message: H256,
|
||||
signature: &Signature,
|
||||
) -> Result<(), BlsError> {
|
||||
let agg_sig = prepare_aggregate_signature(signature)?;
|
||||
let agg_key = prepare_aggregate_pubkey_from_absent(aggregate_pubkey, absent_pubkeys)?;
|
||||
fast_aggregate_verify_pre_aggregated(agg_sig, agg_key, message)
|
||||
}
|
||||
|
||||
/// Decompress one public key into a point in G1.
|
||||
pub fn prepare_milagro_pubkey(pubkey: &PublicKey) -> Result<PublicKeyPrepared, BlsError> {
|
||||
PublicKeyPrepared::from_bytes_unchecked(&pubkey.0).map_err(|_| BlsError::InvalidPublicKey)
|
||||
}
|
||||
|
||||
/// Prepare for G1 public keys.
|
||||
pub fn prepare_g1_pubkeys(pubkeys: &[PublicKey]) -> Result<Vec<PublicKeyPrepared>, BlsError> {
|
||||
pubkeys
|
||||
.iter()
|
||||
// Deserialize one public key from compressed bytes
|
||||
.map(prepare_milagro_pubkey)
|
||||
.collect::<Result<Vec<PublicKeyPrepared>, BlsError>>()
|
||||
}
|
||||
|
||||
/// Prepare for G1 AggregatePublicKey.
|
||||
pub fn prepare_aggregate_pubkey(
|
||||
pubkeys: &[PublicKeyPrepared],
|
||||
) -> Result<AggregatePublicKey, BlsError> {
|
||||
AggregatePublicKey::into_aggregate(pubkeys).map_err(|_| BlsError::InvalidPublicKey)
|
||||
}
|
||||
|
||||
/// Prepare for G1 AggregatePublicKey.
|
||||
pub fn prepare_aggregate_pubkey_from_absent(
|
||||
aggregate_key: &PublicKeyPrepared,
|
||||
absent_pubkeys: &Vec<PublicKeyPrepared>,
|
||||
) -> Result<AggregatePublicKey, BlsError> {
|
||||
let mut aggregate_pubkey = AggregatePublicKey::from_public_key(aggregate_key);
|
||||
if !absent_pubkeys.is_empty() {
|
||||
let absent_aggregate_key = prepare_aggregate_pubkey(absent_pubkeys)?;
|
||||
aggregate_pubkey.point.sub(&absent_aggregate_key.point);
|
||||
}
|
||||
Ok(AggregatePublicKey { point: aggregate_pubkey.point })
|
||||
}
|
||||
|
||||
/// Prepare for G2 AggregateSignature, normally more expensive than G1 operation.
|
||||
pub fn prepare_aggregate_signature(signature: &Signature) -> Result<AggregateSignature, BlsError> {
|
||||
Ok(AggregateSignature::from_signature(
|
||||
&SignaturePrepared::from_bytes(&signature.0).map_err(|_| BlsError::InvalidSignature)?,
|
||||
))
|
||||
}
|
||||
|
||||
/// fast_aggregate_verify_pre_aggregated which is the most expensive call in beacon light client.
|
||||
pub fn fast_aggregate_verify_pre_aggregated(
|
||||
agg_sig: AggregateSignature,
|
||||
aggregate_key: AggregatePublicKey,
|
||||
message: H256,
|
||||
) -> Result<(), BlsError> {
|
||||
ensure!(
|
||||
agg_sig.fast_aggregate_verify_pre_aggregated(&message[..], &aggregate_key),
|
||||
BlsError::SignatureVerificationFailed
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
pub const MAX_PROOF_SIZE: u32 = 20;
|
||||
|
||||
pub const FEE_RECIPIENT_SIZE: usize = 20;
|
||||
pub const EXTRA_DATA_SIZE: usize = 32;
|
||||
pub const LOGS_BLOOM_SIZE: usize = 256;
|
||||
|
||||
pub const PUBKEY_SIZE: usize = 48;
|
||||
pub const SIGNATURE_SIZE: usize = 96;
|
||||
@@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
pub mod bits;
|
||||
pub mod bls;
|
||||
pub mod config;
|
||||
pub mod merkle_proof;
|
||||
pub mod receipt;
|
||||
pub mod ssz;
|
||||
pub mod types;
|
||||
pub mod updates;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
mod serde_utils;
|
||||
|
||||
pub use types::{
|
||||
BeaconHeader, CompactBeaconState, CompactExecutionHeader, ExecutionHeaderState,
|
||||
ExecutionPayloadHeader, FinalizedHeaderState, Fork, ForkData, ForkVersion, ForkVersions, Mode,
|
||||
PublicKey, Signature, SigningData, SyncAggregate, SyncCommittee, SyncCommitteePrepared,
|
||||
};
|
||||
pub use updates::{CheckpointUpdate, ExecutionHeaderUpdate, NextSyncCommitteeUpdate, Update};
|
||||
|
||||
pub use bits::decompress_sync_committee_bits;
|
||||
pub use bls::{
|
||||
fast_aggregate_verify, prepare_aggregate_pubkey, prepare_aggregate_pubkey_from_absent,
|
||||
prepare_aggregate_signature, prepare_g1_pubkeys, AggregatePublicKey, AggregateSignature,
|
||||
BlsError, PublicKeyPrepared, SignaturePrepared,
|
||||
};
|
||||
pub use merkle_proof::verify_merkle_branch;
|
||||
pub use receipt::verify_receipt_proof;
|
||||
@@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use sp_core::H256;
|
||||
use sp_io::hashing::sha2_256;
|
||||
|
||||
/// Specified by <https://github.com/ethereum/consensus-specs/blob/fe9c1a8cbf0c2da8a4f349efdcd77dd7ac8445c4/specs/phase0/beacon-chain.md?plain=1#L742>
|
||||
/// with improvements from <https://github.com/ethereum/consensus-specs/blob/dev/ssz/merkle-proofs.md>
|
||||
pub fn verify_merkle_branch(
|
||||
leaf: H256,
|
||||
branch: &[H256],
|
||||
index: usize,
|
||||
depth: usize,
|
||||
root: H256,
|
||||
) -> bool {
|
||||
// verify the proof length
|
||||
if branch.len() != depth {
|
||||
return false
|
||||
}
|
||||
// verify the computed merkle root
|
||||
root == compute_merkle_root(leaf, branch, index)
|
||||
}
|
||||
|
||||
fn compute_merkle_root(leaf: H256, proof: &[H256], index: usize) -> H256 {
|
||||
let mut value: [u8; 32] = leaf.into();
|
||||
for (i, node) in proof.iter().enumerate() {
|
||||
let mut data = [0u8; 64];
|
||||
if generalized_index_bit(index, i) {
|
||||
// right node
|
||||
data[0..32].copy_from_slice(node.as_bytes());
|
||||
data[32..64].copy_from_slice(&value);
|
||||
value = sha2_256(&data);
|
||||
} else {
|
||||
// left node
|
||||
data[0..32].copy_from_slice(&value);
|
||||
data[32..64].copy_from_slice(node.as_bytes());
|
||||
value = sha2_256(&data);
|
||||
}
|
||||
}
|
||||
value.into()
|
||||
}
|
||||
|
||||
/// Spec: <https://github.com/ethereum/consensus-specs/blob/fe9c1a8cbf0c2da8a4f349efdcd77dd7ac8445c4/ssz/merkle-proofs.md#get_generalized_index_bit>
|
||||
fn generalized_index_bit(index: usize, position: usize) -> bool {
|
||||
index & (1 << position) > 0
|
||||
}
|
||||
|
||||
/// Spec: <https://github.com/ethereum/consensus-specs/blob/fe9c1a8cbf0c2da8a4f349efdcd77dd7ac8445c4/specs/altair/light-client/sync-protocol.md#get_subtree_index>
|
||||
pub const fn subtree_index(generalized_index: usize) -> usize {
|
||||
generalized_index % (1 << generalized_index_length(generalized_index))
|
||||
}
|
||||
|
||||
/// Spec: <https://github.com/ethereum/consensus-specs/blob/fe9c1a8cbf0c2da8a4f349efdcd77dd7ac8445c4/ssz/merkle-proofs.md#get_generalized_index_length>
|
||||
pub const fn generalized_index_length(generalized_index: usize) -> usize {
|
||||
match generalized_index.checked_ilog2() {
|
||||
Some(v) => v as usize,
|
||||
None => panic!("checked statically; qed"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use sp_core::H256;
|
||||
use sp_io::hashing::keccak_256;
|
||||
use sp_std::prelude::*;
|
||||
|
||||
use snowbridge_ethereum::{mpt, Receipt};
|
||||
|
||||
pub fn verify_receipt_proof(
|
||||
receipts_root: H256,
|
||||
proof: &[Vec<u8>],
|
||||
) -> Option<Result<Receipt, rlp::DecoderError>> {
|
||||
match apply_merkle_proof(proof) {
|
||||
Some((root, data)) if root == receipts_root => Some(rlp::decode(&data)),
|
||||
Some((_, _)) => None,
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_merkle_proof(proof: &[Vec<u8>]) -> Option<(H256, Vec<u8>)> {
|
||||
let mut iter = proof.iter().rev();
|
||||
let first_bytes = match iter.next() {
|
||||
Some(b) => b,
|
||||
None => return None,
|
||||
};
|
||||
let item_to_prove: mpt::ShortNode = rlp::decode(first_bytes).ok()?;
|
||||
|
||||
let final_hash: Option<[u8; 32]> = iter.try_fold(keccak_256(first_bytes), |acc, x| {
|
||||
let node: Box<dyn mpt::Node> = x.as_slice().try_into().ok()?;
|
||||
if (*node).contains_hash(acc.into()) {
|
||||
return Some(keccak_256(x))
|
||||
}
|
||||
None
|
||||
});
|
||||
|
||||
final_hash.map(|hash| (hash.into(), item_to_prove.value))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use hex_literal::hex;
|
||||
|
||||
#[test]
|
||||
fn test_verify_receipt_proof() {
|
||||
let root: H256 =
|
||||
hex!("fd5e397a84884641f53c496804f24b5276cbb8c5c9cfc2342246be8e3ce5ad02").into();
|
||||
|
||||
// Valid proof
|
||||
let proof_receipt5 = vec!(
|
||||
hex!("f90131a0b5ba404eb5a6a88e56579f4d37ef9813b5ad7f86f0823ff3b407ac5a6bb465eca0398ead2655e78e03c127ce22c5830e90f18b1601ec055f938336c084feb915a9a026d322c26e46c50942c1aabde50e36df5cde572aed650ce73ea3182c6e90a02ca00600a356135f4db1db0d9842264cdff2652676f881669e91e316c0b6dd783011a0837f1deb4075336da320388c1edfffc56c448a43f4a5ba031300d32a7b509fc5a01c3ac82fd65b4aba7f9afaf604d9c82ec7e2deb573a091ae235751bc5c0c288da05d454159d9071b0f68b6e0503d290f23ac7602c1db0c569dee4605d8f5298f09a00bbed10350ec954448df795f6fd46e3faefc800ede061b3840eedc6e2b07a74da0acb02d26a3650f2064c14a435fdf1f668d8655daf455ebdf671713a7c089b3898080808080808080").to_vec(),
|
||||
hex!("f901f180a00046a08d4f0bdbdc6b31903086ce323182bce6725e7d9415f7ff91ee8f4820bda0e7cd26ad5f3d2771e4b5ab788e268a14a10209f94ee918eb6c829d21d3d11c1da00d4a56d9e9a6751874fd86c7e3cb1c6ad5a848da62751325f478978a00ea966ea064b81920c8f04a8a1e21f53a8280e739fbb7b00b2ab92493ca3f610b70e8ac85a0b1040ed4c55a73178b76abb16f946ce5bebd6b93ab873c83327df54047d12c27a0de6485e9ac58dc6e2b04b4bb38f562684f0b1a2ee586cc11079e7d9a9dc40b32a0d394f4d3532c3124a65fa36e69147e04fd20453a72ee9c50660f17e13ce9df48a066501003fc3e3478efd2803cd0eded6bbe9243ca01ba754d6327071ddbcbc649a0b2684e518f325fee39fc8ea81b68f3f5c785be00d087f3bed8857ae2ee8da26ea071060a5c52042e8d7ce21092f8ecf06053beb9a0b773a6f91a30c4220aa276b2a0fc22436632574ccf6043d0986dede27ea94c9ca9a3bb5ec03ce776a4ddef24a9a05a8a1d6698c4e7d8cc3a2506cb9b12ea9a079c9c7099bc919dc804033cc556e4a0170c468b0716fd36d161f0bf05875f15756a2976de92f9efe7716320509d79c9a0182f909a90cab169f3efb62387f9cccdd61440acc4deec42f68a4f7ca58075c7a055cf0e9202ac75689b76318f1171f3a44465eddc06aae0713bfb6b34fdd27b7980").to_vec(),
|
||||
hex!("f904de20b904daf904d701830652f0b9010004200000000000000000000080020000000000010000000000010000000000000000000000000000000000000000000002000000080000000000000000200000000000000000000000000008000000220000000000400010000000000000000000000000000000000000000000000000000000000000040000000010000100000000000800000000004000000000000000000000000000080000004000000000020000000000020000000000000000000000000000000000000000000004000000000002000000000100000000000000000000000000001000000002000020000010200000000000010000000000000000000000000000000000000010000000f903ccf89b9421130f34829b4c343142047a28ce96ec07814b15f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000007d843005c7433c16b27ff939cb37471541561ebda0000000000000000000000000e9c1281aae66801fa35ec404d5f2aea393ff6988a000000000000000000000000000000000000000000000000000000005d09b7380f89b9421130f34829b4c343142047a28ce96ec07814b15f863a08c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925a00000000000000000000000007d843005c7433c16b27ff939cb37471541561ebda00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da0ffffffffffffffffffffffffffffffffffffffffffffffffffffffcc840c6920f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa0000000000000000000000000e9c1281aae66801fa35ec404d5f2aea393ff6988a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da000000000000000000000000000000000000000000000000003e973b5a5d1078ef87994e9c1281aae66801fa35ec404d5f2aea393ff6988e1a01c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1b840000000000000000000000000000000000000000000000000000001f1420ad1d40000000000000000000000000000000000000000000000014ad400879d159a38f8fc94e9c1281aae66801fa35ec404d5f2aea393ff6988f863a0d78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488db88000000000000000000000000000000000000000000000000000000005d415f3320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e973b5a5d1078ef87a94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a07fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da000000000000000000000000000000000000000000000000003e973b5a5d1078e").to_vec(),
|
||||
);
|
||||
assert!(verify_receipt_proof(root, &proof_receipt5).is_some());
|
||||
|
||||
// Various invalid proofs
|
||||
let proof_empty: Vec<Vec<u8>> = vec![];
|
||||
let proof_missing_full_node = vec![proof_receipt5[0].clone(), proof_receipt5[2].clone()];
|
||||
let proof_missing_short_node1 = vec![proof_receipt5[0].clone(), proof_receipt5[1].clone()];
|
||||
let proof_missing_short_node2 = vec![proof_receipt5[0].clone()];
|
||||
let proof_invalid_encoding = vec![proof_receipt5[2][2..].to_vec()];
|
||||
let proof_no_full_node = vec![proof_receipt5[2].clone(), proof_receipt5[2].clone()];
|
||||
assert!(verify_receipt_proof(root, &proof_empty).is_none());
|
||||
assert!(verify_receipt_proof(root, &proof_missing_full_node).is_none());
|
||||
|
||||
assert_eq!(
|
||||
verify_receipt_proof(root, &proof_missing_short_node1),
|
||||
Some(Err(rlp::DecoderError::Custom("Unsupported receipt type")))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
verify_receipt_proof(root, &proof_missing_short_node2),
|
||||
Some(Err(rlp::DecoderError::Custom("Unsupported receipt type")))
|
||||
);
|
||||
|
||||
assert!(verify_receipt_proof(root, &proof_invalid_encoding).is_none());
|
||||
assert!(verify_receipt_proof(root, &proof_no_full_node).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_receipt_proof_with_intermediate_short_node() {
|
||||
let root: H256 =
|
||||
hex!("d128e3a57142d2bf15bc0cbcac7ad54f40750d571b5c3097e425882c10c9ba66").into();
|
||||
|
||||
let proof_receipt263 = vec![
|
||||
hex!("f90131a00d3cb8d3f57ac1c0e12918a2ebe0cafed8c273577b9dd73e7ed1079b403ef494a0678b9835b834f8a287c0dd33a8fca9146e456ca688555ed4ec1361a2180b778da0fe42da181a46677a043b3d9d4b8bb05a6a17b7b5c010c17e7c1d31cfb7c4f911a0c89f0e2c53241cdb578e1f2b4caf6ba36e00500bdc57fecd66b84a6a58394c19a086c3c1fae5a0575940b5d38e111c469d07883106c26856f3ef608469a2081f13a06c5992ff00aab6226a70a032fd2f571ba22f797321f45e2daa73020d638d21b0a050861e9503ef68728f6c90a44f7fe1bceb2a9bdab6957bbe7136166bd849561ea006aa6eaca8a07e57176e9aa41e6a09edfb7678d1a112404e0ec779d7e567e82ea0bb0b430d303ba21b0af11c487b8a218bd75db54c98940b3f11bad8ff47cad3ef8080808080808080").to_vec(),
|
||||
hex!("f871a0246de222036ee6a03329b0105da0a6b3f916fc95a9ed5a403a581a0c4d74242ca0ac108a49a88b57a05ac34a108b39f1e45f6f167f2b9fbc8d52fb58e2e5a6af1ea0fcfe07ac2ccd3c28b6eab68d1bce112f6f6dbd9023e4ec3c05b96615aa803d798080808080808080808080808080").to_vec(),
|
||||
hex!("e4820001a04fff54398cad4d05ea6abfd8b0f3b4fe14c04d7ff5f5211c5b927d9cf72ac1d8").to_vec(),
|
||||
hex!("f851a096d010643ca2d47412ca66898286b5f2412963b9ec051b33e570d575914c9c5ca028cd24c652989542fe89479ec6388eac4592432242af5ba97563b3ac7c71c019808080808080808080808080808080").to_vec(),
|
||||
hex!("f90211a0bb35a84c5b1dcb78ec9d32614912c696e62df77bebf9ab326ee55b5d3acdde46a01084b30dac8df0accfcd0fd6330b7f6fc72a4651246d0694be9162151686a620a03eed50afdce7909d784c6157c445a444c806b5f23d31f3b63786f600c84a95b2a0af5232f1df6c6d41879804d081abe867002abe26ba3e5f8e0254a83a54769831a0607915fb13dd5da594256389a45007a67a7f7a86e95d38d8462792b6c98a722ea00e1260fda1730f2738c650ce2bfba83857bc10f8fb119ebc4fb39acba24e6fbaa0d11de17e417327457812675ca3b84ae8e1b64827abfe01420953697c8313d5b1a05fcaf2f7a88f76336a0c32ffc78acb87ae2005454bd25d658035331be3173b46a03f94f4952ab9e650f83cfd0e7f367b1bcc493aacf39a06f16c4a2e1b5605da48a0bdb4ec79785ca8ae22d60f1bbd42d707b4d7ec4aff231a3ebab755e315b35053a043a67c3f2bcef37c8f47a673adcb7061007a553696d1092408601c11b2e6846aa0c519d5af48cae87c7f4538845417c9735813bee892a6fe2dda79f5c414e8576aa0f7058256e09589501d7c231d739e61c84a850e139690989d24fda6058b432e98a081a52faab520978cb19ce14400dba0cd5bcdc4e5a3c0740678aa8f97ee0e5c56a0bcecc61cadeae52518e3b68a48af4b11603dfd9d99d99d7985efa6d2de44f904a02cba4accfc6f39bc5adb6d4440eb6358b4a5103ef93298e4e694f1f940f8b48280").to_vec(),
|
||||
hex!("f901ae20b901aaf901a70183bb444eb9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000001000000000000000000000000000100000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000010000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000080000000000000000000000000000000000000000000000002000000000000000000081000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000f89df89b94dac17f958d2ee523a2206206994597c13d831ec7f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000002e514404ff6823f1b46a8318a709251db414e5e1a000000000000000000000000055021c55847c00d764357a352e5803237d328954a0000000000000000000000000000000000000000000000000000000000201c370").to_vec(),
|
||||
];
|
||||
assert!(verify_receipt_proof(root, &proof_receipt263).is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use sp_core::U256;
|
||||
|
||||
use core::fmt::Formatter;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
// helper to deserialize arbitrary arrays like [T; N]
|
||||
pub mod arrays {
|
||||
use std::{convert::TryInto, marker::PhantomData};
|
||||
|
||||
use serde::{
|
||||
de::{SeqAccess, Visitor},
|
||||
ser::SerializeTuple,
|
||||
Deserialize, Deserializer, Serialize, Serializer,
|
||||
};
|
||||
|
||||
pub fn serialize<S: Serializer, T: Serialize, const N: usize>(
|
||||
data: &[T; N],
|
||||
ser: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
let mut s = ser.serialize_tuple(N)?;
|
||||
for item in data {
|
||||
s.serialize_element(item)?;
|
||||
}
|
||||
s.end()
|
||||
}
|
||||
|
||||
struct ArrayVisitor<T, const N: usize>(PhantomData<T>);
|
||||
|
||||
impl<'de, T, const N: usize> Visitor<'de> for ArrayVisitor<T, N>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
type Value = [T; N];
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str(&format!("an array of length {}", N))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
// can be optimized using MaybeUninit
|
||||
let mut data = Vec::with_capacity(N);
|
||||
for _ in 0..N {
|
||||
match (seq.next_element())? {
|
||||
Some(val) => data.push(val),
|
||||
None => return Err(serde::de::Error::invalid_length(N, &self)),
|
||||
}
|
||||
}
|
||||
match data.try_into() {
|
||||
Ok(arr) => Ok(arr),
|
||||
Err(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D, T, const N: usize>(deserializer: D) -> Result<[T; N], D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
deserializer.deserialize_tuple(N, ArrayVisitor::<T, N>(PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_hex_to_bytes<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
|
||||
let str_without_0x = match s.strip_prefix("0x") {
|
||||
Some(val) => val,
|
||||
None => &s,
|
||||
};
|
||||
|
||||
let hex_bytes = match hex::decode(str_without_0x) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => return Err(serde::de::Error::custom(e.to_string())),
|
||||
};
|
||||
|
||||
Ok(hex_bytes)
|
||||
}
|
||||
|
||||
pub(crate) fn from_int_to_u256<'de, D>(deserializer: D) -> Result<U256, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let number = u128::deserialize(deserializer)?;
|
||||
|
||||
Ok(U256::from(number))
|
||||
}
|
||||
|
||||
pub struct HexVisitor<const LENGTH: usize>();
|
||||
|
||||
impl<'de, const LENGTH: usize> serde::de::Visitor<'de> for HexVisitor<LENGTH> {
|
||||
type Value = [u8; LENGTH];
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> sp_std::fmt::Result {
|
||||
formatter.write_str("a hex string with an '0x' prefix")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
let stripped = match v.strip_prefix("0x") {
|
||||
Some(stripped) => stripped,
|
||||
None => v,
|
||||
};
|
||||
|
||||
let decoded = match hex::decode(stripped) {
|
||||
Ok(decoded) => decoded,
|
||||
Err(e) => return Err(serde::de::Error::custom(e.to_string())),
|
||||
};
|
||||
if decoded.len() != LENGTH {
|
||||
return Err(serde::de::Error::custom("publickey expected to be 48 characters"))
|
||||
}
|
||||
|
||||
let data: Self::Value = decoded
|
||||
.try_into()
|
||||
.map_err(|_e| serde::de::Error::custom("hex data has unexpected length"))?;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use crate::{
|
||||
config::{EXTRA_DATA_SIZE, FEE_RECIPIENT_SIZE, LOGS_BLOOM_SIZE, PUBKEY_SIZE, SIGNATURE_SIZE},
|
||||
types::{
|
||||
BeaconHeader, ExecutionPayloadHeader, ForkData, SigningData, SyncAggregate, SyncCommittee,
|
||||
},
|
||||
};
|
||||
use byte_slice_cast::AsByteSlice;
|
||||
use sp_core::H256;
|
||||
use sp_std::{vec, vec::Vec};
|
||||
use ssz_rs::{
|
||||
prelude::{List, Vector},
|
||||
Bitvector, Deserialize, DeserializeError, SimpleSerialize, SimpleSerializeError, Sized, U256,
|
||||
};
|
||||
use ssz_rs_derive::SimpleSerialize as SimpleSerializeDerive;
|
||||
|
||||
#[derive(Default, SimpleSerializeDerive, Clone, Debug)]
|
||||
pub struct SSZBeaconBlockHeader {
|
||||
pub slot: u64,
|
||||
pub proposer_index: u64,
|
||||
pub parent_root: [u8; 32],
|
||||
pub state_root: [u8; 32],
|
||||
pub body_root: [u8; 32],
|
||||
}
|
||||
|
||||
impl From<BeaconHeader> for SSZBeaconBlockHeader {
|
||||
fn from(beacon_header: BeaconHeader) -> Self {
|
||||
SSZBeaconBlockHeader {
|
||||
slot: beacon_header.slot,
|
||||
proposer_index: beacon_header.proposer_index,
|
||||
parent_root: beacon_header.parent_root.to_fixed_bytes(),
|
||||
state_root: beacon_header.state_root.to_fixed_bytes(),
|
||||
body_root: beacon_header.body_root.to_fixed_bytes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, SimpleSerializeDerive, Clone)]
|
||||
pub struct SSZSyncCommittee<const COMMITTEE_SIZE: usize> {
|
||||
pub pubkeys: Vector<Vector<u8, PUBKEY_SIZE>, COMMITTEE_SIZE>,
|
||||
pub aggregate_pubkey: Vector<u8, PUBKEY_SIZE>,
|
||||
}
|
||||
|
||||
impl<const COMMITTEE_SIZE: usize> From<SyncCommittee<COMMITTEE_SIZE>>
|
||||
for SSZSyncCommittee<COMMITTEE_SIZE>
|
||||
{
|
||||
fn from(sync_committee: SyncCommittee<COMMITTEE_SIZE>) -> Self {
|
||||
let mut pubkeys_vec = Vec::new();
|
||||
|
||||
for pubkey in sync_committee.pubkeys.iter() {
|
||||
// The only thing that can go wrong in the conversion from vec to Vector (ssz type) is
|
||||
// that the Vector size is 0, or that the given data to create the Vector from does not
|
||||
// match the expected size N. Because these sizes are statically checked (i.e.
|
||||
// PublicKey's size is 48, and const PUBKEY_SIZE is 48, it is impossible for "try_from"
|
||||
// to return an error condition.
|
||||
let conv_pubkey = Vector::<u8, PUBKEY_SIZE>::try_from(pubkey.0.to_vec())
|
||||
.expect("checked statically; qed");
|
||||
|
||||
pubkeys_vec.push(conv_pubkey);
|
||||
}
|
||||
|
||||
let pubkeys = Vector::<Vector<u8, PUBKEY_SIZE>, { COMMITTEE_SIZE }>::try_from(pubkeys_vec)
|
||||
.expect("checked statically; qed");
|
||||
|
||||
let aggregate_pubkey =
|
||||
Vector::<u8, PUBKEY_SIZE>::try_from(sync_committee.aggregate_pubkey.0.to_vec())
|
||||
.expect("checked statically; qed");
|
||||
|
||||
SSZSyncCommittee { pubkeys, aggregate_pubkey }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, SimpleSerializeDerive, Clone)]
|
||||
pub struct SSZSyncAggregate<const COMMITTEE_SIZE: usize> {
|
||||
pub sync_committee_bits: Bitvector<COMMITTEE_SIZE>,
|
||||
pub sync_committee_signature: Vector<u8, SIGNATURE_SIZE>,
|
||||
}
|
||||
|
||||
impl<const COMMITTEE_SIZE: usize, const COMMITTEE_BITS_SIZE: usize>
|
||||
From<SyncAggregate<COMMITTEE_SIZE, COMMITTEE_BITS_SIZE>> for SSZSyncAggregate<COMMITTEE_SIZE>
|
||||
{
|
||||
fn from(sync_aggregate: SyncAggregate<COMMITTEE_SIZE, COMMITTEE_BITS_SIZE>) -> Self {
|
||||
SSZSyncAggregate {
|
||||
sync_committee_bits: Bitvector::<COMMITTEE_SIZE>::deserialize(
|
||||
&sync_aggregate.sync_committee_bits,
|
||||
)
|
||||
.expect("checked statically; qed"),
|
||||
sync_committee_signature: Vector::<u8, SIGNATURE_SIZE>::try_from(
|
||||
sync_aggregate.sync_committee_signature.0.to_vec(),
|
||||
)
|
||||
.expect("checked statically; qed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, SimpleSerializeDerive, Clone)]
|
||||
pub struct SSZForkData {
|
||||
pub current_version: [u8; 4],
|
||||
pub genesis_validators_root: [u8; 32],
|
||||
}
|
||||
|
||||
impl From<ForkData> for SSZForkData {
|
||||
fn from(fork_data: ForkData) -> Self {
|
||||
SSZForkData {
|
||||
current_version: fork_data.current_version,
|
||||
genesis_validators_root: fork_data.genesis_validators_root,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, SimpleSerializeDerive, Clone)]
|
||||
pub struct SSZSigningData {
|
||||
pub object_root: [u8; 32],
|
||||
pub domain: [u8; 32],
|
||||
}
|
||||
|
||||
impl From<SigningData> for SSZSigningData {
|
||||
fn from(signing_data: SigningData) -> Self {
|
||||
SSZSigningData {
|
||||
object_root: signing_data.object_root.into(),
|
||||
domain: signing_data.domain.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, SimpleSerializeDerive, Clone, Debug)]
|
||||
pub struct SSZExecutionPayloadHeader {
|
||||
pub parent_hash: [u8; 32],
|
||||
pub fee_recipient: Vector<u8, FEE_RECIPIENT_SIZE>,
|
||||
pub state_root: [u8; 32],
|
||||
pub receipts_root: [u8; 32],
|
||||
pub logs_bloom: Vector<u8, LOGS_BLOOM_SIZE>,
|
||||
pub prev_randao: [u8; 32],
|
||||
pub block_number: u64,
|
||||
pub gas_limit: u64,
|
||||
pub gas_used: u64,
|
||||
pub timestamp: u64,
|
||||
pub extra_data: List<u8, EXTRA_DATA_SIZE>,
|
||||
pub base_fee_per_gas: U256,
|
||||
pub block_hash: [u8; 32],
|
||||
pub transactions_root: [u8; 32],
|
||||
pub withdrawals_root: [u8; 32],
|
||||
}
|
||||
|
||||
impl TryFrom<ExecutionPayloadHeader> for SSZExecutionPayloadHeader {
|
||||
type Error = SimpleSerializeError;
|
||||
|
||||
fn try_from(payload: ExecutionPayloadHeader) -> Result<Self, Self::Error> {
|
||||
Ok(SSZExecutionPayloadHeader {
|
||||
parent_hash: payload.parent_hash.to_fixed_bytes(),
|
||||
fee_recipient: Vector::<u8, FEE_RECIPIENT_SIZE>::try_from(
|
||||
payload.fee_recipient.to_fixed_bytes().to_vec(),
|
||||
)
|
||||
.expect("checked statically; qed"),
|
||||
state_root: payload.state_root.to_fixed_bytes(),
|
||||
receipts_root: payload.receipts_root.to_fixed_bytes(),
|
||||
// Logs bloom bytes size is not constrained, so here we do need to check the try_from
|
||||
// error
|
||||
logs_bloom: Vector::<u8, LOGS_BLOOM_SIZE>::try_from(payload.logs_bloom)
|
||||
.map_err(|(_, err)| err)?,
|
||||
prev_randao: payload.prev_randao.to_fixed_bytes(),
|
||||
block_number: payload.block_number,
|
||||
gas_limit: payload.gas_limit,
|
||||
gas_used: payload.gas_used,
|
||||
timestamp: payload.timestamp,
|
||||
// Extra data bytes size is not constrained, so here we do need to check the try_from
|
||||
// error
|
||||
extra_data: List::<u8, EXTRA_DATA_SIZE>::try_from(payload.extra_data)
|
||||
.map_err(|(_, err)| err)?,
|
||||
base_fee_per_gas: U256::from_bytes_le(
|
||||
payload
|
||||
.base_fee_per_gas
|
||||
.as_byte_slice()
|
||||
.try_into()
|
||||
.expect("checked in prep; qed"),
|
||||
),
|
||||
block_hash: payload.block_hash.to_fixed_bytes(),
|
||||
transactions_root: payload.transactions_root.to_fixed_bytes(),
|
||||
withdrawals_root: payload.withdrawals_root.to_fixed_bytes(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash_tree_root<T: SimpleSerialize>(mut object: T) -> Result<H256, SimpleSerializeError> {
|
||||
match object.hash_tree_root() {
|
||||
Ok(node) => {
|
||||
let fixed_bytes: [u8; 32] =
|
||||
node.as_ref().try_into().expect("Node is a newtype over [u8; 32]; qed");
|
||||
Ok(fixed_bytes.into())
|
||||
},
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use frame_support::{CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::{H160, H256, U256};
|
||||
use sp_runtime::RuntimeDebug;
|
||||
use sp_std::{boxed::Box, prelude::*};
|
||||
|
||||
use crate::config::{PUBKEY_SIZE, SIGNATURE_SIZE};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use crate::serde_utils::HexVisitor;
|
||||
|
||||
use crate::ssz::{
|
||||
hash_tree_root, SSZBeaconBlockHeader, SSZExecutionPayloadHeader, SSZForkData, SSZSigningData,
|
||||
SSZSyncAggregate, SSZSyncCommittee,
|
||||
};
|
||||
use ssz_rs::SimpleSerializeError;
|
||||
|
||||
pub use crate::bits::decompress_sync_committee_bits;
|
||||
|
||||
use crate::bls::{prepare_g1_pubkeys, prepare_milagro_pubkey, BlsError};
|
||||
use milagro_bls::PublicKey as PublicKeyPrepared;
|
||||
|
||||
pub type ValidatorIndex = u64;
|
||||
pub type ForkVersion = [u8; 4];
|
||||
|
||||
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct ForkVersions {
|
||||
pub genesis: Fork,
|
||||
pub altair: Fork,
|
||||
pub bellatrix: Fork,
|
||||
pub capella: Fork,
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct Fork {
|
||||
pub version: [u8; 4],
|
||||
pub epoch: u64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct PublicKey(pub [u8; PUBKEY_SIZE]);
|
||||
|
||||
impl Default for PublicKey {
|
||||
fn default() -> Self {
|
||||
PublicKey([0u8; PUBKEY_SIZE])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; PUBKEY_SIZE]> for PublicKey {
|
||||
fn from(v: [u8; PUBKEY_SIZE]) -> Self {
|
||||
Self(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl MaxEncodedLen for PublicKey {
|
||||
fn max_encoded_len() -> usize {
|
||||
PUBKEY_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<'de> Deserialize<'de> for PublicKey {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(HexVisitor::<PUBKEY_SIZE>()).map(|v| v.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl Serialize for PublicKey {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_bytes(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct Signature(pub [u8; SIGNATURE_SIZE]);
|
||||
|
||||
impl Default for Signature {
|
||||
fn default() -> Self {
|
||||
Signature([0u8; SIGNATURE_SIZE])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; SIGNATURE_SIZE]> for Signature {
|
||||
fn from(v: [u8; SIGNATURE_SIZE]) -> Self {
|
||||
Self(v)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<'de> Deserialize<'de> for Signature {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(HexVisitor::<SIGNATURE_SIZE>()).map(|v| v.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Encode, Decode, TypeInfo, MaxEncodedLen)]
|
||||
pub struct ExecutionHeaderState {
|
||||
pub beacon_block_root: H256,
|
||||
pub beacon_slot: u64,
|
||||
pub block_hash: H256,
|
||||
pub block_number: u64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Encode, Decode, TypeInfo, MaxEncodedLen)]
|
||||
pub struct FinalizedHeaderState {
|
||||
pub beacon_block_root: H256,
|
||||
pub beacon_slot: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Encode, Decode, PartialEq, RuntimeDebug)]
|
||||
pub struct ForkData {
|
||||
// 1 or 0 bit, indicates whether a sync committee participated in a vote
|
||||
pub current_version: [u8; 4],
|
||||
pub genesis_validators_root: [u8; 32],
|
||||
}
|
||||
|
||||
impl ForkData {
|
||||
pub fn hash_tree_root(&self) -> Result<H256, SimpleSerializeError> {
|
||||
hash_tree_root::<SSZForkData>(self.clone().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Encode, Decode, PartialEq, RuntimeDebug)]
|
||||
pub struct SigningData {
|
||||
pub object_root: H256,
|
||||
pub domain: H256,
|
||||
}
|
||||
|
||||
impl SigningData {
|
||||
pub fn hash_tree_root(&self) -> Result<H256, SimpleSerializeError> {
|
||||
hash_tree_root::<SSZSigningData>(self.clone().into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync committee as it is stored in the runtime storage.
|
||||
#[derive(
|
||||
Encode, Decode, PartialEqNoBound, CloneNoBound, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen,
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
derive(Serialize, Deserialize),
|
||||
serde(deny_unknown_fields, bound(serialize = ""), bound(deserialize = ""))
|
||||
)]
|
||||
#[codec(mel_bound())]
|
||||
pub struct SyncCommittee<const COMMITTEE_SIZE: usize> {
|
||||
#[cfg_attr(feature = "std", serde(with = "crate::serde_utils::arrays"))]
|
||||
pub pubkeys: [PublicKey; COMMITTEE_SIZE],
|
||||
pub aggregate_pubkey: PublicKey,
|
||||
}
|
||||
|
||||
impl<const COMMITTEE_SIZE: usize> Default for SyncCommittee<COMMITTEE_SIZE> {
|
||||
fn default() -> Self {
|
||||
SyncCommittee {
|
||||
pubkeys: [Default::default(); COMMITTEE_SIZE],
|
||||
aggregate_pubkey: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const COMMITTEE_SIZE: usize> SyncCommittee<COMMITTEE_SIZE> {
|
||||
pub fn hash_tree_root(&self) -> Result<H256, SimpleSerializeError> {
|
||||
hash_tree_root::<SSZSyncCommittee<COMMITTEE_SIZE>>(self.clone().into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepared G1 public key of sync committee as it is stored in the runtime storage.
|
||||
#[derive(Clone, PartialEq, Eq, Encode, Decode, TypeInfo, MaxEncodedLen)]
|
||||
pub struct SyncCommitteePrepared<const COMMITTEE_SIZE: usize> {
|
||||
pub root: H256,
|
||||
pub pubkeys: Box<[PublicKeyPrepared; COMMITTEE_SIZE]>,
|
||||
pub aggregate_pubkey: PublicKeyPrepared,
|
||||
}
|
||||
|
||||
impl<const COMMITTEE_SIZE: usize> Default for SyncCommitteePrepared<COMMITTEE_SIZE> {
|
||||
fn default() -> Self {
|
||||
SyncCommitteePrepared {
|
||||
root: H256::default(),
|
||||
pubkeys: Box::new([PublicKeyPrepared::default(); COMMITTEE_SIZE]),
|
||||
aggregate_pubkey: PublicKeyPrepared::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const COMMITTEE_SIZE: usize> TryFrom<&SyncCommittee<COMMITTEE_SIZE>>
|
||||
for SyncCommitteePrepared<COMMITTEE_SIZE>
|
||||
{
|
||||
type Error = BlsError;
|
||||
|
||||
fn try_from(sync_committee: &SyncCommittee<COMMITTEE_SIZE>) -> Result<Self, Self::Error> {
|
||||
let g1_pubkeys = prepare_g1_pubkeys(&sync_committee.pubkeys)?;
|
||||
let sync_committee_root = sync_committee.hash_tree_root().expect("checked statically; qed");
|
||||
|
||||
Ok(SyncCommitteePrepared::<COMMITTEE_SIZE> {
|
||||
pubkeys: g1_pubkeys.try_into().expect("checked statically; qed"),
|
||||
aggregate_pubkey: prepare_milagro_pubkey(&sync_committee.aggregate_pubkey)?,
|
||||
root: sync_committee_root,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Beacon block header as it is stored in the runtime storage. The block root is the
|
||||
/// Merkleization of a BeaconHeader.
|
||||
#[derive(
|
||||
Copy, Clone, Default, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen,
|
||||
)]
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||
pub struct BeaconHeader {
|
||||
// The slot for which this block is created. Must be greater than the slot of the block defined
|
||||
// by parent root.
|
||||
pub slot: u64,
|
||||
// The index of the validator that proposed the block.
|
||||
pub proposer_index: ValidatorIndex,
|
||||
// The block root of the parent block, forming a block chain.
|
||||
pub parent_root: H256,
|
||||
// The hash root of the post state of running the state transition through this block.
|
||||
pub state_root: H256,
|
||||
// The hash root of the beacon block body
|
||||
pub body_root: H256,
|
||||
}
|
||||
|
||||
impl BeaconHeader {
|
||||
pub fn hash_tree_root(&self) -> Result<H256, SimpleSerializeError> {
|
||||
hash_tree_root::<SSZBeaconBlockHeader>((*self).into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo)]
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
derive(Deserialize),
|
||||
serde(
|
||||
try_from = "IntermediateSyncAggregate",
|
||||
deny_unknown_fields,
|
||||
bound(serialize = ""),
|
||||
bound(deserialize = "")
|
||||
)
|
||||
)]
|
||||
#[codec(mel_bound())]
|
||||
pub struct SyncAggregate<const COMMITTEE_SIZE: usize, const COMMITTEE_BITS_SIZE: usize> {
|
||||
pub sync_committee_bits: [u8; COMMITTEE_BITS_SIZE],
|
||||
pub sync_committee_signature: Signature,
|
||||
}
|
||||
|
||||
impl<const COMMITTEE_SIZE: usize, const COMMITTEE_BITS_SIZE: usize> Default
|
||||
for SyncAggregate<COMMITTEE_SIZE, COMMITTEE_BITS_SIZE>
|
||||
{
|
||||
fn default() -> Self {
|
||||
SyncAggregate {
|
||||
sync_committee_bits: [0; COMMITTEE_BITS_SIZE],
|
||||
sync_committee_signature: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const COMMITTEE_SIZE: usize, const COMMITTEE_BITS_SIZE: usize>
|
||||
SyncAggregate<COMMITTEE_SIZE, COMMITTEE_BITS_SIZE>
|
||||
{
|
||||
pub fn hash_tree_root(&self) -> Result<H256, SimpleSerializeError> {
|
||||
hash_tree_root::<SSZSyncAggregate<COMMITTEE_SIZE>>(self.clone().into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Serde deserialization helper for SyncAggregate
|
||||
#[cfg(feature = "std")]
|
||||
#[derive(Deserialize)]
|
||||
struct IntermediateSyncAggregate {
|
||||
#[cfg_attr(feature = "std", serde(deserialize_with = "crate::serde_utils::from_hex_to_bytes"))]
|
||||
pub sync_committee_bits: Vec<u8>,
|
||||
pub sync_committee_signature: Signature,
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<const COMMITTEE_SIZE: usize, const COMMITTEE_BITS_SIZE: usize>
|
||||
TryFrom<IntermediateSyncAggregate> for SyncAggregate<COMMITTEE_SIZE, COMMITTEE_BITS_SIZE>
|
||||
{
|
||||
type Error = String;
|
||||
|
||||
fn try_from(other: IntermediateSyncAggregate) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
sync_committee_bits: other
|
||||
.sync_committee_bits
|
||||
.try_into()
|
||||
.map_err(|_| "unexpected length".to_owned())?,
|
||||
sync_committee_signature: other.sync_committee_signature,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// ExecutionPayloadHeader
|
||||
/// <https://github.com/ethereum/annotated-spec/blob/master/capella/beacon-chain.md#executionpayloadheader>
|
||||
#[derive(
|
||||
Default, Encode, Decode, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo,
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
derive(Deserialize),
|
||||
serde(deny_unknown_fields, bound(serialize = ""), bound(deserialize = ""))
|
||||
)]
|
||||
#[codec(mel_bound())]
|
||||
pub struct ExecutionPayloadHeader {
|
||||
pub parent_hash: H256,
|
||||
pub fee_recipient: H160,
|
||||
pub state_root: H256,
|
||||
pub receipts_root: H256,
|
||||
#[cfg_attr(feature = "std", serde(deserialize_with = "crate::serde_utils::from_hex_to_bytes"))]
|
||||
pub logs_bloom: Vec<u8>,
|
||||
pub prev_randao: H256,
|
||||
pub block_number: u64,
|
||||
pub gas_limit: u64,
|
||||
pub gas_used: u64,
|
||||
pub timestamp: u64,
|
||||
#[cfg_attr(feature = "std", serde(deserialize_with = "crate::serde_utils::from_hex_to_bytes"))]
|
||||
pub extra_data: Vec<u8>,
|
||||
#[cfg_attr(feature = "std", serde(deserialize_with = "crate::serde_utils::from_int_to_u256"))]
|
||||
pub base_fee_per_gas: U256,
|
||||
pub block_hash: H256,
|
||||
pub transactions_root: H256,
|
||||
pub withdrawals_root: H256,
|
||||
}
|
||||
|
||||
impl ExecutionPayloadHeader {
|
||||
pub fn hash_tree_root(&self) -> Result<H256, SimpleSerializeError> {
|
||||
hash_tree_root::<SSZExecutionPayloadHeader>(self.clone().try_into()?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Default,
|
||||
Encode,
|
||||
Decode,
|
||||
CloneNoBound,
|
||||
PartialEqNoBound,
|
||||
RuntimeDebugNoBound,
|
||||
TypeInfo,
|
||||
MaxEncodedLen,
|
||||
)]
|
||||
pub struct CompactExecutionHeader {
|
||||
pub parent_hash: H256,
|
||||
#[codec(compact)]
|
||||
pub block_number: u64,
|
||||
pub state_root: H256,
|
||||
pub receipts_root: H256,
|
||||
}
|
||||
|
||||
impl From<ExecutionPayloadHeader> for CompactExecutionHeader {
|
||||
fn from(execution_payload: ExecutionPayloadHeader) -> Self {
|
||||
Self {
|
||||
parent_hash: execution_payload.parent_hash,
|
||||
block_number: execution_payload.block_number,
|
||||
state_root: execution_payload.state_root,
|
||||
receipts_root: execution_payload.receipts_root,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Default,
|
||||
Encode,
|
||||
Decode,
|
||||
Copy,
|
||||
Clone,
|
||||
PartialEqNoBound,
|
||||
RuntimeDebugNoBound,
|
||||
TypeInfo,
|
||||
MaxEncodedLen,
|
||||
)]
|
||||
pub struct CompactBeaconState {
|
||||
#[codec(compact)]
|
||||
pub slot: u64,
|
||||
pub block_roots_root: H256,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use hex_literal::hex;
|
||||
|
||||
#[test]
|
||||
pub fn test_hash_beacon_header1() {
|
||||
let hash_root = BeaconHeader {
|
||||
slot: 3,
|
||||
proposer_index: 2,
|
||||
parent_root: hex!("796ea53efb534eab7777809cc5ee2d84e7f25024b9d0c4d7e5bcaab657e4bdbd")
|
||||
.into(),
|
||||
state_root: hex!("ba3ff080912be5c9c158b2e962c1b39a91bc0615762ba6fa2ecacafa94e9ae0a")
|
||||
.into(),
|
||||
body_root: hex!("a18d7fcefbb74a177c959160e0ee89c23546482154e6831237710414465dcae5")
|
||||
.into(),
|
||||
}
|
||||
.hash_tree_root();
|
||||
|
||||
assert!(hash_root.is_ok());
|
||||
assert_eq!(
|
||||
hash_root.unwrap(),
|
||||
hex!("7d42595818709e805dd2fa710a2d2c1f62576ef1ab7273941ac9130fb94b91f7").into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_hash_beacon_header2() {
|
||||
let hash_root = BeaconHeader {
|
||||
slot: 3476424,
|
||||
proposer_index: 314905,
|
||||
parent_root: hex!("c069d7b49cffd2b815b0fb8007eb9ca91202ea548df6f3db60000f29b2489f28")
|
||||
.into(),
|
||||
state_root: hex!("444d293e4533501ee508ad608783a7d677c3c566f001313e8a02ce08adf590a3")
|
||||
.into(),
|
||||
body_root: hex!("6508a0241047f21ba88f05d05b15534156ab6a6f8e029a9a5423da429834e04a")
|
||||
.into(),
|
||||
}
|
||||
.hash_tree_root();
|
||||
|
||||
assert!(hash_root.is_ok());
|
||||
assert_eq!(
|
||||
hash_root.unwrap(),
|
||||
hex!("0aa41166ff01e58e111ac8c42309a738ab453cf8d7285ed8477b1c484acb123e").into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_hash_fork_data() {
|
||||
let hash_root = ForkData {
|
||||
current_version: hex!("83f38a34"),
|
||||
genesis_validators_root: hex!(
|
||||
"22370bbbb358800f5711a10ea9845284272d8493bed0348cab87b8ab1e127930"
|
||||
),
|
||||
}
|
||||
.hash_tree_root();
|
||||
|
||||
assert!(hash_root.is_ok());
|
||||
assert_eq!(
|
||||
hash_root.unwrap(),
|
||||
hex!("57c12c4246bc7152b174b51920506bf943eff9c7ffa50b9533708e9cc1f680fc").into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_hash_signing_data() {
|
||||
let hash_root = SigningData {
|
||||
object_root: hex!("63654cbe64fc07853f1198c165dd3d49c54fc53bc417989bbcc66da15f850c54")
|
||||
.into(),
|
||||
domain: hex!("037da907d1c3a03c0091b2254e1480d9b1783476e228ab29adaaa8f133e08f7a").into(),
|
||||
}
|
||||
.hash_tree_root();
|
||||
|
||||
assert!(hash_root.is_ok());
|
||||
assert_eq!(
|
||||
hash_root.unwrap(),
|
||||
hex!("b9eb2caf2d691b183c2d57f322afe505c078cd08101324f61c3641714789a54e").into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_hash_sync_aggregate() {
|
||||
let hash_root = SyncAggregate::<512, 64>{
|
||||
sync_committee_bits: hex!("cefffffefffffff767fffbedffffeffffeeffdffffdebffffff7f7dbdf7fffdffffbffcfffdff79dfffbbfefff2ffffff7ddeff7ffffc98ff7fbfffffffffff7"),
|
||||
sync_committee_signature: hex!("8af1a8577bba419fe054ee49b16ed28e081dda6d3ba41651634685e890992a0b675e20f8d9f2ec137fe9eb50e838aa6117f9f5410e2e1024c4b4f0e098e55144843ce90b7acde52fe7b94f2a1037342c951dc59f501c92acf7ed944cb6d2b5f7").into(),
|
||||
}.hash_tree_root();
|
||||
|
||||
assert!(hash_root.is_ok());
|
||||
assert_eq!(
|
||||
hash_root.unwrap(),
|
||||
hex!("e6dcad4f60ce9ff8a587b110facbaf94721f06cd810b6d8bf6cffa641272808d").into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_hash_execution_payload() {
|
||||
let hash_root =
|
||||
ExecutionPayloadHeader{
|
||||
parent_hash: hex!("eadee5ab098dde64e9fd02ae5858064bad67064070679625b09f8d82dec183f7").into(),
|
||||
fee_recipient: hex!("f97e180c050e5ab072211ad2c213eb5aee4df134").into(),
|
||||
state_root: hex!("564fa064c2a324c2b5978d7fdfc5d4224d4f421a45388af1ed405a399c845dff").into(),
|
||||
receipts_root: hex!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").into(),
|
||||
logs_bloom: hex!("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").to_vec(),
|
||||
prev_randao: hex!("6bf538bdfbdf1c96ff528726a40658a91d0bda0f1351448c4c4f3604db2a0ccf").into(),
|
||||
block_number: 477434,
|
||||
gas_limit: 8154925,
|
||||
gas_used: 0,
|
||||
timestamp: 1652816940,
|
||||
extra_data: vec![],
|
||||
base_fee_per_gas: U256::from(7_i16),
|
||||
block_hash: hex!("cd8df91b4503adb8f2f1c7a4f60e07a1f1a2cbdfa2a95bceba581f3ff65c1968").into(),
|
||||
transactions_root: hex!("7ffe241ea60187fdb0187bfa22de35d1f9bed7ab061d9401fd47e34a54fbede1").into(),
|
||||
withdrawals_root: hex!("28ba1834a3a7b657460ce79fa3a1d909ab8828fd557659d4d0554a9bdbc0ec30").into(),
|
||||
}.hash_tree_root();
|
||||
assert!(hash_root.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
/// Operating modes for beacon client
|
||||
#[derive(Encode, Decode, Copy, Clone, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub enum Mode {
|
||||
Active,
|
||||
Blocked,
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::{CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::H256;
|
||||
use sp_std::prelude::*;
|
||||
|
||||
use crate::types::{BeaconHeader, ExecutionPayloadHeader, SyncAggregate, SyncCommittee};
|
||||
|
||||
#[derive(Encode, Decode, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo)]
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(deny_unknown_fields, bound(serialize = ""), bound(deserialize = ""))
|
||||
)]
|
||||
pub struct CheckpointUpdate<const COMMITTEE_SIZE: usize> {
|
||||
pub header: BeaconHeader,
|
||||
pub current_sync_committee: SyncCommittee<COMMITTEE_SIZE>,
|
||||
pub current_sync_committee_branch: Vec<H256>,
|
||||
pub validators_root: H256,
|
||||
pub block_roots_root: H256,
|
||||
pub block_roots_branch: Vec<H256>,
|
||||
}
|
||||
|
||||
impl<const COMMITTEE_SIZE: usize> Default for CheckpointUpdate<COMMITTEE_SIZE> {
|
||||
fn default() -> Self {
|
||||
CheckpointUpdate {
|
||||
header: Default::default(),
|
||||
current_sync_committee: Default::default(),
|
||||
current_sync_committee_branch: Default::default(),
|
||||
validators_root: Default::default(),
|
||||
block_roots_root: Default::default(),
|
||||
block_roots_branch: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Default, Encode, Decode, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo,
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
derive(serde::Deserialize),
|
||||
serde(deny_unknown_fields, bound(serialize = ""), bound(deserialize = ""))
|
||||
)]
|
||||
pub struct Update<const COMMITTEE_SIZE: usize, const COMMITTEE_BITS_SIZE: usize> {
|
||||
/// A recent header attesting to the finalized header, using its `state_root`.
|
||||
pub attested_header: BeaconHeader,
|
||||
/// The signing data that the sync committee produced for this attested header, including
|
||||
/// who participated in the vote and the resulting signature.
|
||||
pub sync_aggregate: SyncAggregate<COMMITTEE_SIZE, COMMITTEE_BITS_SIZE>,
|
||||
/// The slot at which the sync aggregate can be found, typically attested_header.slot + 1, if
|
||||
/// the next slot block was not missed.
|
||||
pub signature_slot: u64,
|
||||
/// The next sync committee for the next sync committee period, if present.
|
||||
pub next_sync_committee_update: Option<NextSyncCommitteeUpdate<COMMITTEE_SIZE>>,
|
||||
/// The latest finalized header.
|
||||
pub finalized_header: BeaconHeader,
|
||||
/// The merkle proof testifying to the finalized header, using the `attested_header.state_root`
|
||||
/// as tree root.
|
||||
pub finality_branch: Vec<H256>,
|
||||
/// The finalized_header's `block_roots` root in the beacon state, used for ancestry proofs.
|
||||
pub block_roots_root: H256,
|
||||
/// The merkle path to prove the `block_roots_root` value.
|
||||
pub block_roots_branch: Vec<H256>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Default, Encode, Decode, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo,
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
derive(serde::Deserialize),
|
||||
serde(deny_unknown_fields, bound(serialize = ""), bound(deserialize = ""))
|
||||
)]
|
||||
pub struct NextSyncCommitteeUpdate<const COMMITTEE_SIZE: usize> {
|
||||
pub next_sync_committee: SyncCommittee<COMMITTEE_SIZE>,
|
||||
pub next_sync_committee_branch: Vec<H256>,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo)]
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
derive(serde::Deserialize),
|
||||
serde(deny_unknown_fields, bound(serialize = ""), bound(deserialize = ""))
|
||||
)]
|
||||
pub struct ExecutionHeaderUpdate {
|
||||
/// Header for the beacon block containing the execution payload
|
||||
pub header: BeaconHeader,
|
||||
/// Proof that `header` is an ancestor of a finalized header
|
||||
pub ancestry_proof: Option<AncestryProof>,
|
||||
/// Execution header to be imported
|
||||
pub execution_header: ExecutionPayloadHeader,
|
||||
/// Merkle proof that execution payload is contained within `header`
|
||||
pub execution_branch: Vec<H256>,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo)]
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
derive(serde::Deserialize),
|
||||
serde(deny_unknown_fields, bound(serialize = ""), bound(deserialize = ""))
|
||||
)]
|
||||
pub struct AncestryProof {
|
||||
/// Merkle proof that `header` is an ancestor of `finalized_header`
|
||||
pub header_branch: Vec<H256>,
|
||||
/// Root of a finalized block that has already been imported into the light client
|
||||
pub finalized_block_root: H256,
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
[package]
|
||||
name = "snowbridge-core"
|
||||
description = "Snowbridge Core"
|
||||
version = "0.1.1"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.188", optional = true, features = ["alloc", "derive"], default-features = false }
|
||||
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false }
|
||||
scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
|
||||
hex-literal = { version = "0.4.1" }
|
||||
|
||||
polkadot-parachain-primitives = { path = "../../../../../polkadot/parachain", default-features = false }
|
||||
xcm = { package = "staging-xcm", path = "../../../../../polkadot/xcm", default-features = false }
|
||||
xcm-builder = { package = "staging-xcm-builder", path = "../../../../../polkadot/xcm/xcm-builder", default-features = false }
|
||||
|
||||
frame-support = { path = "../../../../../substrate/frame/support", default-features = false }
|
||||
frame-system = { path = "../../../../../substrate/frame/system", default-features = false }
|
||||
sp-runtime = { path = "../../../../../substrate/primitives/runtime", default-features = false }
|
||||
sp-std = { path = "../../../../../substrate/primitives/std", default-features = false }
|
||||
sp-io = { path = "../../../../../substrate/primitives/io", default-features = false }
|
||||
sp-core = { path = "../../../../../substrate/primitives/core", default-features = false }
|
||||
sp-arithmetic = { path = "../../../../../substrate/primitives/arithmetic", default-features = false }
|
||||
|
||||
snowbridge-beacon-primitives = { path = "../../primitives/beacon", default-features = false }
|
||||
|
||||
ethabi = { git = "https://github.com/Snowfork/ethabi-decode.git", package = "ethabi-decode", branch = "master", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = { version = "0.4.3" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"ethabi/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"polkadot-parachain-primitives/std",
|
||||
"scale-info/std",
|
||||
"serde/std",
|
||||
"snowbridge-beacon-primitives/std",
|
||||
"sp-arithmetic/std",
|
||||
"sp-core/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
"xcm-builder/std",
|
||||
"xcm/std",
|
||||
]
|
||||
serde = ["dep:serde", "scale-info/serde"]
|
||||
runtime-benchmarks = [
|
||||
"frame-support/runtime-benchmarks",
|
||||
"frame-system/runtime-benchmarks",
|
||||
"polkadot-parachain-primitives/runtime-benchmarks",
|
||||
"sp-runtime/runtime-benchmarks",
|
||||
"xcm-builder/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,74 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Types for representing inbound messages
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::PalletError;
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::{H160, H256};
|
||||
use sp_runtime::RuntimeDebug;
|
||||
use sp_std::vec::Vec;
|
||||
|
||||
/// A trait for verifying inbound messages from Ethereum.
|
||||
pub trait Verifier {
|
||||
fn verify(event: &Log, proof: &Proof) -> Result<(), VerificationError>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug, PalletError, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub enum VerificationError {
|
||||
/// Execution header is missing
|
||||
HeaderNotFound,
|
||||
/// Event log was not found in the verified transaction receipt
|
||||
LogNotFound,
|
||||
/// Event log has an invalid format
|
||||
InvalidLog,
|
||||
/// Unable to verify the transaction receipt with the provided proof
|
||||
InvalidProof,
|
||||
}
|
||||
|
||||
pub type MessageNonce = u64;
|
||||
|
||||
/// A bridge message from the Gateway contract on Ethereum
|
||||
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct Message {
|
||||
/// Event log emitted by Gateway contract
|
||||
pub event_log: Log,
|
||||
/// Inclusion proof for a transaction receipt containing the event log
|
||||
pub proof: Proof,
|
||||
}
|
||||
|
||||
const MAX_TOPICS: usize = 4;
|
||||
|
||||
#[derive(Clone, RuntimeDebug)]
|
||||
pub enum LogValidationError {
|
||||
TooManyTopics,
|
||||
}
|
||||
|
||||
/// Event log
|
||||
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct Log {
|
||||
pub address: H160,
|
||||
pub topics: Vec<H256>,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Log {
|
||||
pub fn validate(&self) -> Result<(), LogValidationError> {
|
||||
if self.topics.len() > MAX_TOPICS {
|
||||
return Err(LogValidationError::TooManyTopics)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Inclusion proof for a transaction receipt
|
||||
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct Proof {
|
||||
// The block hash of the block in which the receipt was included.
|
||||
pub block_hash: H256,
|
||||
// The index of the transaction (and receipt) within the block.
|
||||
pub tx_index: u32,
|
||||
// Proof keys and values (receipts tree)
|
||||
pub data: (Vec<Vec<u8>>, Vec<Vec<u8>>),
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! # Core
|
||||
//!
|
||||
//! Common traits and types
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub mod inbound;
|
||||
pub mod operating_mode;
|
||||
pub mod outbound;
|
||||
pub mod pricing;
|
||||
pub mod ringbuffer;
|
||||
|
||||
pub use polkadot_parachain_primitives::primitives::{
|
||||
Id as ParaId, IsSystem, Sibling as SiblingParaId,
|
||||
};
|
||||
pub use ringbuffer::{RingBufferMap, RingBufferMapImpl};
|
||||
pub use sp_core::U256;
|
||||
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use frame_support::traits::Contains;
|
||||
use hex_literal::hex;
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::H256;
|
||||
use sp_io::hashing::keccak_256;
|
||||
use sp_runtime::{traits::AccountIdConversion, RuntimeDebug};
|
||||
use sp_std::prelude::*;
|
||||
use xcm::prelude::{
|
||||
Junction::Parachain,
|
||||
Junctions::{Here, X1},
|
||||
MultiLocation,
|
||||
};
|
||||
use xcm_builder::{DescribeAllTerminal, DescribeFamily, DescribeLocation, HashedDescription};
|
||||
|
||||
/// The ID of an agent contract
|
||||
pub type AgentId = H256;
|
||||
pub use operating_mode::BasicOperatingMode;
|
||||
|
||||
pub use pricing::{PricingParameters, Rewards};
|
||||
|
||||
pub fn sibling_sovereign_account<T>(para_id: ParaId) -> T::AccountId
|
||||
where
|
||||
T: frame_system::Config,
|
||||
{
|
||||
SiblingParaId::from(para_id).into_account_truncating()
|
||||
}
|
||||
|
||||
pub fn sibling_sovereign_account_raw(para_id: ParaId) -> [u8; 32] {
|
||||
SiblingParaId::from(para_id).into_account_truncating()
|
||||
}
|
||||
|
||||
pub struct AllowSiblingsOnly;
|
||||
impl Contains<MultiLocation> for AllowSiblingsOnly {
|
||||
fn contains(location: &MultiLocation) -> bool {
|
||||
matches!(location, MultiLocation { parents: 1, interior: X1(Parachain(_)) })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gwei(x: u128) -> U256 {
|
||||
U256::from(1_000_000_000u128).saturating_mul(x.into())
|
||||
}
|
||||
|
||||
pub fn meth(x: u128) -> U256 {
|
||||
U256::from(1_000_000_000_000_000u128).saturating_mul(x.into())
|
||||
}
|
||||
|
||||
pub fn eth(x: u128) -> U256 {
|
||||
U256::from(1_000_000_000_000_000_000u128).saturating_mul(x.into())
|
||||
}
|
||||
|
||||
pub const ROC: u128 = 1_000_000_000_000;
|
||||
|
||||
/// Identifier for a message channel
|
||||
#[derive(
|
||||
Clone, Copy, Encode, Decode, PartialEq, Eq, Default, RuntimeDebug, MaxEncodedLen, TypeInfo,
|
||||
)]
|
||||
pub struct ChannelId([u8; 32]);
|
||||
|
||||
/// Deterministically derive a ChannelId for a sibling parachain
|
||||
/// Generator: keccak256("para" + big_endian_bytes(para_id))
|
||||
///
|
||||
/// The equivalent generator on the Solidity side is in
|
||||
/// contracts/src/Types.sol:into().
|
||||
fn derive_channel_id_for_sibling(para_id: ParaId) -> ChannelId {
|
||||
let para_id: u32 = para_id.into();
|
||||
let para_id_bytes: [u8; 4] = para_id.to_be_bytes();
|
||||
let prefix: [u8; 4] = *b"para";
|
||||
let preimage: Vec<u8> = prefix.into_iter().chain(para_id_bytes).collect();
|
||||
keccak_256(&preimage).into()
|
||||
}
|
||||
|
||||
impl ChannelId {
|
||||
pub const fn new(id: [u8; 32]) -> Self {
|
||||
ChannelId(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParaId> for ChannelId {
|
||||
fn from(value: ParaId) -> Self {
|
||||
derive_channel_id_for_sibling(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; 32]> for ChannelId {
|
||||
fn from(value: [u8; 32]) -> Self {
|
||||
ChannelId(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChannelId> for [u8; 32] {
|
||||
fn from(value: ChannelId) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [u8; 32]> for ChannelId {
|
||||
fn from(value: &'a [u8; 32]) -> Self {
|
||||
ChannelId(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<H256> for ChannelId {
|
||||
fn from(value: H256) -> Self {
|
||||
ChannelId(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for ChannelId {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug, MaxEncodedLen, TypeInfo)]
|
||||
pub struct Channel {
|
||||
/// ID of the agent contract deployed on Ethereum
|
||||
pub agent_id: AgentId,
|
||||
/// ID of the parachain who will receive or send messages using this channel
|
||||
pub para_id: ParaId,
|
||||
}
|
||||
|
||||
pub trait StaticLookup {
|
||||
/// Type to lookup from.
|
||||
type Source;
|
||||
/// Type to lookup into.
|
||||
type Target;
|
||||
/// Attempt a lookup.
|
||||
fn lookup(s: Self::Source) -> Option<Self::Target>;
|
||||
}
|
||||
|
||||
/// Channel for high-priority governance commands
|
||||
pub const PRIMARY_GOVERNANCE_CHANNEL: ChannelId =
|
||||
ChannelId::new(hex!("0000000000000000000000000000000000000000000000000000000000000001"));
|
||||
|
||||
/// Channel for lower-priority governance commands
|
||||
pub const SECONDARY_GOVERNANCE_CHANNEL: ChannelId =
|
||||
ChannelId::new(hex!("0000000000000000000000000000000000000000000000000000000000000002"));
|
||||
|
||||
pub struct DescribeHere;
|
||||
impl DescribeLocation for DescribeHere {
|
||||
fn describe_location(l: &MultiLocation) -> Option<Vec<u8>> {
|
||||
match (l.parents, l.interior) {
|
||||
(0, Here) => Some(Vec::<u8>::new().encode()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an AgentId from a MultiLocation. An AgentId is a unique mapping to a Agent contract on
|
||||
/// Ethereum which acts as the sovereign account for the MultiLocation.
|
||||
pub type AgentIdOf = HashedDescription<H256, (DescribeHere, DescribeFamily<DescribeAllTerminal>)>;
|
||||
@@ -0,0 +1,25 @@
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_runtime::RuntimeDebug;
|
||||
|
||||
/// Basic operating modes for a bridges module (Normal/Halted).
|
||||
#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum BasicOperatingMode {
|
||||
/// Normal mode, when all operations are allowed.
|
||||
Normal,
|
||||
/// The pallet is halted. All non-governance operations are disabled.
|
||||
Halted,
|
||||
}
|
||||
|
||||
impl Default for BasicOperatingMode {
|
||||
fn default() -> Self {
|
||||
Self::Normal
|
||||
}
|
||||
}
|
||||
|
||||
impl BasicOperatingMode {
|
||||
pub fn is_halted(&self) -> bool {
|
||||
*self == BasicOperatingMode::Halted
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::PalletError;
|
||||
use scale_info::TypeInfo;
|
||||
use sp_arithmetic::traits::{BaseArithmetic, Unsigned};
|
||||
use sp_core::{RuntimeDebug, H256};
|
||||
pub use v1::{AgentExecuteCommand, Command, Initializer, Message, OperatingMode, QueuedMessage};
|
||||
|
||||
/// Enqueued outbound messages need to be versioned to prevent data corruption
|
||||
/// or loss after forkless runtime upgrades
|
||||
#[derive(Encode, Decode, TypeInfo, Clone, RuntimeDebug)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub enum VersionedQueuedMessage {
|
||||
V1(QueuedMessage),
|
||||
}
|
||||
|
||||
impl TryFrom<VersionedQueuedMessage> for QueuedMessage {
|
||||
type Error = ();
|
||||
fn try_from(x: VersionedQueuedMessage) -> Result<Self, Self::Error> {
|
||||
use VersionedQueuedMessage::*;
|
||||
match x {
|
||||
V1(x) => Ok(x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<QueuedMessage>> From<T> for VersionedQueuedMessage {
|
||||
fn from(x: T) -> Self {
|
||||
VersionedQueuedMessage::V1(x.into())
|
||||
}
|
||||
}
|
||||
|
||||
mod v1 {
|
||||
use crate::{pricing::UD60x18, ChannelId};
|
||||
use codec::{Decode, Encode};
|
||||
use ethabi::Token;
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::{RuntimeDebug, H160, H256, U256};
|
||||
use sp_std::{borrow::ToOwned, vec, vec::Vec};
|
||||
|
||||
/// A message which can be accepted by implementations of `/[`SendMessage`\]`
|
||||
#[derive(Encode, Decode, TypeInfo, Clone, RuntimeDebug)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub struct Message {
|
||||
/// ID for this message. One will be automatically generated if not provided.
|
||||
///
|
||||
/// When this message is created from an XCM message, the ID should be extracted
|
||||
/// from the `SetTopic` instruction.
|
||||
///
|
||||
/// The ID plays no role in bridge consensus, and is purely meant for message tracing.
|
||||
pub id: Option<H256>,
|
||||
/// The message channel ID
|
||||
pub channel_id: ChannelId,
|
||||
/// The stable ID for a receiving gateway contract
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
/// The operating mode of Channels and Gateway contract on Ethereum.
|
||||
#[derive(Copy, Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, TypeInfo)]
|
||||
pub enum OperatingMode {
|
||||
/// Normal operations. Allow sending and receiving messages.
|
||||
Normal,
|
||||
/// Reject outbound messages. This allows receiving governance messages but does now allow
|
||||
/// enqueuing of new messages from the Ethereum side. This can be used to close off an
|
||||
/// deprecated channel or pause the bridge for upgrade operations.
|
||||
RejectingOutboundMessages,
|
||||
}
|
||||
|
||||
/// A command which is executable by the Gateway contract on Ethereum
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub enum Command {
|
||||
/// Execute a sub-command within an agent for a consensus system in Polkadot
|
||||
AgentExecute {
|
||||
/// The ID of the agent
|
||||
agent_id: H256,
|
||||
/// The sub-command to be executed
|
||||
command: AgentExecuteCommand,
|
||||
},
|
||||
/// Upgrade the Gateway contract
|
||||
Upgrade {
|
||||
/// Address of the new implementation contract
|
||||
impl_address: H160,
|
||||
/// Codehash of the implementation contract
|
||||
impl_code_hash: H256,
|
||||
/// Optionally invoke an initializer in the implementation contract
|
||||
initializer: Option<Initializer>,
|
||||
},
|
||||
/// Create an agent representing a consensus system on Polkadot
|
||||
CreateAgent {
|
||||
/// The ID of the agent, derived from the `MultiLocation` of the consensus system on
|
||||
/// Polkadot
|
||||
agent_id: H256,
|
||||
},
|
||||
/// Create bidirectional messaging channel to a parachain
|
||||
CreateChannel {
|
||||
/// The ID of the channel
|
||||
channel_id: ChannelId,
|
||||
/// The agent ID of the parachain
|
||||
agent_id: H256,
|
||||
/// Initial operating mode
|
||||
mode: OperatingMode,
|
||||
},
|
||||
/// Update the configuration of a channel
|
||||
UpdateChannel {
|
||||
/// The ID of the channel
|
||||
channel_id: ChannelId,
|
||||
/// The new operating mode
|
||||
mode: OperatingMode,
|
||||
},
|
||||
/// Set the global operating mode of the Gateway contract
|
||||
SetOperatingMode {
|
||||
/// The new operating mode
|
||||
mode: OperatingMode,
|
||||
},
|
||||
/// Transfer ether from an agent contract to a recipient account
|
||||
TransferNativeFromAgent {
|
||||
/// The agent ID
|
||||
agent_id: H256,
|
||||
/// The recipient of the ether
|
||||
recipient: H160,
|
||||
/// The amount to transfer
|
||||
amount: u128,
|
||||
},
|
||||
/// Set token fees of the Gateway contract
|
||||
SetTokenTransferFees {
|
||||
/// The fee(DOT) for the cost of creating asset on AssetHub
|
||||
create_asset_xcm: u128,
|
||||
/// The fee(DOT) for the cost of sending asset on AssetHub
|
||||
transfer_asset_xcm: u128,
|
||||
/// The fee(Ether) for register token to discourage spamming
|
||||
register_token: U256,
|
||||
},
|
||||
/// Set pricing parameters
|
||||
SetPricingParameters {
|
||||
// ETH/DOT exchange rate
|
||||
exchange_rate: UD60x18,
|
||||
// Cost of delivering a message from Ethereum to BridgeHub, in ROC/KSM/DOT
|
||||
delivery_cost: u128,
|
||||
},
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Compute the enum variant index
|
||||
pub fn index(&self) -> u8 {
|
||||
match self {
|
||||
Command::AgentExecute { .. } => 0,
|
||||
Command::Upgrade { .. } => 1,
|
||||
Command::CreateAgent { .. } => 2,
|
||||
Command::CreateChannel { .. } => 3,
|
||||
Command::UpdateChannel { .. } => 4,
|
||||
Command::SetOperatingMode { .. } => 5,
|
||||
Command::TransferNativeFromAgent { .. } => 6,
|
||||
Command::SetTokenTransferFees { .. } => 7,
|
||||
Command::SetPricingParameters { .. } => 8,
|
||||
}
|
||||
}
|
||||
|
||||
/// ABI-encode the Command.
|
||||
pub fn abi_encode(&self) -> Vec<u8> {
|
||||
match self {
|
||||
Command::AgentExecute { agent_id, command } =>
|
||||
ethabi::encode(&[Token::Tuple(vec![
|
||||
Token::FixedBytes(agent_id.as_bytes().to_owned()),
|
||||
Token::Bytes(command.abi_encode()),
|
||||
])]),
|
||||
Command::Upgrade { impl_address, impl_code_hash, initializer, .. } =>
|
||||
ethabi::encode(&[Token::Tuple(vec![
|
||||
Token::Address(*impl_address),
|
||||
Token::FixedBytes(impl_code_hash.as_bytes().to_owned()),
|
||||
initializer
|
||||
.clone()
|
||||
.map_or(Token::Bytes(vec![]), |i| Token::Bytes(i.params)),
|
||||
])]),
|
||||
Command::CreateAgent { agent_id } =>
|
||||
ethabi::encode(&[Token::Tuple(vec![Token::FixedBytes(
|
||||
agent_id.as_bytes().to_owned(),
|
||||
)])]),
|
||||
Command::CreateChannel { channel_id, agent_id, mode } =>
|
||||
ethabi::encode(&[Token::Tuple(vec![
|
||||
Token::FixedBytes(channel_id.as_ref().to_owned()),
|
||||
Token::FixedBytes(agent_id.as_bytes().to_owned()),
|
||||
Token::Uint(U256::from((*mode) as u64)),
|
||||
])]),
|
||||
Command::UpdateChannel { channel_id, mode } =>
|
||||
ethabi::encode(&[Token::Tuple(vec![
|
||||
Token::FixedBytes(channel_id.as_ref().to_owned()),
|
||||
Token::Uint(U256::from((*mode) as u64)),
|
||||
])]),
|
||||
Command::SetOperatingMode { mode } =>
|
||||
ethabi::encode(&[Token::Tuple(vec![Token::Uint(U256::from((*mode) as u64))])]),
|
||||
Command::TransferNativeFromAgent { agent_id, recipient, amount } =>
|
||||
ethabi::encode(&[Token::Tuple(vec![
|
||||
Token::FixedBytes(agent_id.as_bytes().to_owned()),
|
||||
Token::Address(*recipient),
|
||||
Token::Uint(U256::from(*amount)),
|
||||
])]),
|
||||
Command::SetTokenTransferFees {
|
||||
create_asset_xcm,
|
||||
transfer_asset_xcm,
|
||||
register_token,
|
||||
} => ethabi::encode(&[Token::Tuple(vec![
|
||||
Token::Uint(U256::from(*create_asset_xcm)),
|
||||
Token::Uint(U256::from(*transfer_asset_xcm)),
|
||||
Token::Uint(*register_token),
|
||||
])]),
|
||||
Command::SetPricingParameters { exchange_rate, delivery_cost } =>
|
||||
ethabi::encode(&[Token::Tuple(vec![
|
||||
Token::Uint(exchange_rate.clone().into_inner()),
|
||||
Token::Uint(U256::from(*delivery_cost)),
|
||||
])]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Representation of a call to the initializer of an implementation contract.
|
||||
/// The initializer has the following ABI signature: `initialize(bytes)`.
|
||||
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct Initializer {
|
||||
/// ABI-encoded params of type `bytes` to pass to the initializer
|
||||
pub params: Vec<u8>,
|
||||
/// The initializer is allowed to consume this much gas at most.
|
||||
pub maximum_required_gas: u64,
|
||||
}
|
||||
|
||||
/// A Sub-command executable within an agent
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub enum AgentExecuteCommand {
|
||||
/// Transfer ERC20 tokens
|
||||
TransferToken {
|
||||
/// Address of the ERC20 token
|
||||
token: H160,
|
||||
/// The recipient of the tokens
|
||||
recipient: H160,
|
||||
/// The amount of tokens to transfer
|
||||
amount: u128,
|
||||
},
|
||||
}
|
||||
|
||||
impl AgentExecuteCommand {
|
||||
fn index(&self) -> u8 {
|
||||
match self {
|
||||
AgentExecuteCommand::TransferToken { .. } => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// ABI-encode the sub-command
|
||||
pub fn abi_encode(&self) -> Vec<u8> {
|
||||
match self {
|
||||
AgentExecuteCommand::TransferToken { token, recipient, amount } =>
|
||||
ethabi::encode(&[
|
||||
Token::Uint(self.index().into()),
|
||||
Token::Bytes(ethabi::encode(&[
|
||||
Token::Address(*token),
|
||||
Token::Address(*recipient),
|
||||
Token::Uint(U256::from(*amount)),
|
||||
])),
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Message which is awaiting processing in the MessageQueue pallet
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub struct QueuedMessage {
|
||||
/// Message ID
|
||||
pub id: H256,
|
||||
/// Channel ID
|
||||
pub channel_id: ChannelId,
|
||||
/// Command to execute in the Gateway contract
|
||||
pub command: Command,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "std", derive(PartialEq, Debug))]
|
||||
/// Fee for delivering message
|
||||
pub struct Fee<Balance>
|
||||
where
|
||||
Balance: BaseArithmetic + Unsigned + Copy,
|
||||
{
|
||||
/// Fee to cover cost of processing the message locally
|
||||
pub local: Balance,
|
||||
/// Fee to cover cost processing the message remotely
|
||||
pub remote: Balance,
|
||||
}
|
||||
|
||||
impl<Balance> Fee<Balance>
|
||||
where
|
||||
Balance: BaseArithmetic + Unsigned + Copy,
|
||||
{
|
||||
pub fn total(&self) -> Balance {
|
||||
self.local.saturating_add(self.remote)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Balance> From<(Balance, Balance)> for Fee<Balance>
|
||||
where
|
||||
Balance: BaseArithmetic + Unsigned + Copy,
|
||||
{
|
||||
fn from((local, remote): (Balance, Balance)) -> Self {
|
||||
Self { local, remote }
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for sending messages to Ethereum
|
||||
pub trait SendMessage: SendMessageFeeProvider {
|
||||
type Ticket: Clone + Encode + Decode;
|
||||
|
||||
/// Validate an outbound message and return a tuple:
|
||||
/// 1. Ticket for submitting the message
|
||||
/// 2. Delivery fee
|
||||
fn validate(
|
||||
message: &Message,
|
||||
) -> Result<(Self::Ticket, Fee<<Self as SendMessageFeeProvider>::Balance>), SendError>;
|
||||
|
||||
/// Submit the message ticket for eventual delivery to Ethereum
|
||||
fn deliver(ticket: Self::Ticket) -> Result<H256, SendError>;
|
||||
}
|
||||
|
||||
pub trait Ticket: Encode + Decode + Clone {
|
||||
fn message_id(&self) -> H256;
|
||||
}
|
||||
|
||||
/// A trait for getting the local costs associated with sending a message.
|
||||
pub trait SendMessageFeeProvider {
|
||||
type Balance: BaseArithmetic + Unsigned + Copy;
|
||||
|
||||
/// The local component of the message processing fees in native currency
|
||||
fn local_fee() -> Self::Balance;
|
||||
}
|
||||
|
||||
/// Reasons why sending to Ethereum could not be initiated
|
||||
#[derive(Copy, Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, PalletError, TypeInfo)]
|
||||
pub enum SendError {
|
||||
/// Message is too large to be safely executed on Ethereum
|
||||
MessageTooLarge,
|
||||
/// The bridge has been halted for maintenance
|
||||
Halted,
|
||||
/// Invalid Channel
|
||||
InvalidChannel,
|
||||
}
|
||||
|
||||
pub trait GasMeter {
|
||||
/// All the gas used for submitting a message to Ethereum, minus the cost of dispatching
|
||||
/// the command within the message
|
||||
const MAXIMUM_BASE_GAS: u64;
|
||||
|
||||
fn maximum_gas_used_at_most(command: &Command) -> u64 {
|
||||
Self::MAXIMUM_BASE_GAS + Self::maximum_dispatch_gas_used_at_most(command)
|
||||
}
|
||||
|
||||
/// Measures the maximum amount of gas a command payload will require to dispatch, AFTER
|
||||
/// validation & verification.
|
||||
fn maximum_dispatch_gas_used_at_most(command: &Command) -> u64;
|
||||
}
|
||||
|
||||
/// A meter that assigns a constant amount of gas for the execution of a command
|
||||
///
|
||||
/// The gas figures are extracted from this report:
|
||||
/// > forge test --match-path test/Gateway.t.sol --gas-report
|
||||
///
|
||||
/// A healthy buffer is added on top of these figures to account for:
|
||||
/// * The EIP-150 63/64 rule
|
||||
/// * Future EVM upgrades that may increase gas cost
|
||||
pub struct ConstantGasMeter;
|
||||
|
||||
impl GasMeter for ConstantGasMeter {
|
||||
// The base transaction cost, which includes:
|
||||
// 21_000 transaction cost, roughly worst case 64_000 for calldata, and 100_000
|
||||
// for message verification
|
||||
const MAXIMUM_BASE_GAS: u64 = 185_000;
|
||||
|
||||
fn maximum_dispatch_gas_used_at_most(command: &Command) -> u64 {
|
||||
match command {
|
||||
Command::CreateAgent { .. } => 275_000,
|
||||
Command::CreateChannel { .. } => 100_000,
|
||||
Command::UpdateChannel { .. } => 50_000,
|
||||
Command::TransferNativeFromAgent { .. } => 60_000,
|
||||
Command::SetOperatingMode { .. } => 40_000,
|
||||
Command::AgentExecute { command, .. } => match command {
|
||||
// Execute IERC20.transferFrom
|
||||
//
|
||||
// Worst-case assumptions are important:
|
||||
// * No gas refund for clearing storage slot of source account in ERC20 contract
|
||||
// * Assume dest account in ERC20 contract does not yet have a storage slot
|
||||
// * ERC20.transferFrom possibly does other business logic besides updating balances
|
||||
AgentExecuteCommand::TransferToken { .. } => 100_000,
|
||||
},
|
||||
Command::Upgrade { initializer, .. } => {
|
||||
let initializer_max_gas = match *initializer {
|
||||
Some(Initializer { maximum_required_gas, .. }) => maximum_required_gas,
|
||||
None => 0,
|
||||
};
|
||||
// total maximum gas must also include the gas used for updating the proxy before
|
||||
// the the initializer is called.
|
||||
50_000 + initializer_max_gas
|
||||
},
|
||||
Command::SetTokenTransferFees { .. } => 60_000,
|
||||
Command::SetPricingParameters { .. } => 60_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GasMeter for () {
|
||||
const MAXIMUM_BASE_GAS: u64 = 1;
|
||||
|
||||
fn maximum_dispatch_gas_used_at_most(_: &Command) -> u64 {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
pub const ETHER_DECIMALS: u8 = 18;
|
||||
@@ -0,0 +1,67 @@
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_arithmetic::traits::{BaseArithmetic, Unsigned, Zero};
|
||||
use sp_core::U256;
|
||||
use sp_runtime::{FixedU128, RuntimeDebug};
|
||||
use sp_std::prelude::*;
|
||||
|
||||
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
|
||||
pub struct PricingParameters<Balance> {
|
||||
/// ETH/DOT exchange rate
|
||||
pub exchange_rate: FixedU128,
|
||||
/// Relayer rewards
|
||||
pub rewards: Rewards<Balance>,
|
||||
/// Ether (wei) fee per gas unit
|
||||
pub fee_per_gas: U256,
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
|
||||
pub struct Rewards<Balance> {
|
||||
/// Local reward in DOT
|
||||
pub local: Balance,
|
||||
/// Remote reward in ETH (wei)
|
||||
pub remote: U256,
|
||||
}
|
||||
|
||||
#[derive(RuntimeDebug)]
|
||||
pub struct InvalidPricingParameters;
|
||||
|
||||
impl<Balance> PricingParameters<Balance>
|
||||
where
|
||||
Balance: BaseArithmetic + Unsigned + Copy,
|
||||
{
|
||||
pub fn validate(&self) -> Result<(), InvalidPricingParameters> {
|
||||
if self.exchange_rate == FixedU128::zero() {
|
||||
return Err(InvalidPricingParameters)
|
||||
}
|
||||
if self.fee_per_gas == U256::zero() {
|
||||
return Err(InvalidPricingParameters)
|
||||
}
|
||||
if self.rewards.local.is_zero() {
|
||||
return Err(InvalidPricingParameters)
|
||||
}
|
||||
if self.rewards.remote.is_zero() {
|
||||
return Err(InvalidPricingParameters)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Holder for fixed point number implemented in <https://github.com/PaulRBerg/prb-math>
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub struct UD60x18(U256);
|
||||
|
||||
impl From<FixedU128> for UD60x18 {
|
||||
fn from(value: FixedU128) -> Self {
|
||||
// Both FixedU128 and UD60x18 have 18 decimal places
|
||||
let inner: u128 = value.into_inner();
|
||||
UD60x18(inner.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl UD60x18 {
|
||||
pub fn into_inner(self) -> U256 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use codec::FullCodec;
|
||||
use core::{cmp::Ord, marker::PhantomData, ops::Add};
|
||||
use frame_support::storage::{types::QueryKindTrait, StorageMap, StorageValue};
|
||||
use sp_core::{Get, GetDefault};
|
||||
use sp_runtime::traits::{One, Zero};
|
||||
|
||||
/// Trait object presenting the ringbuffer interface.
|
||||
pub trait RingBufferMap<Key, Value, QueryKind>
|
||||
where
|
||||
Key: FullCodec,
|
||||
Value: FullCodec,
|
||||
QueryKind: QueryKindTrait<Value, GetDefault>,
|
||||
{
|
||||
/// Insert a map entry.
|
||||
fn insert(k: Key, v: Value);
|
||||
|
||||
/// Check if map contains a key
|
||||
fn contains_key(k: Key) -> bool;
|
||||
|
||||
/// Get the value of the key
|
||||
fn get(k: Key) -> QueryKind::Query;
|
||||
}
|
||||
|
||||
pub struct RingBufferMapImpl<Index, B, CurrentIndex, Intermediate, M, QueryKind>(
|
||||
PhantomData<(Index, B, CurrentIndex, Intermediate, M, QueryKind)>,
|
||||
);
|
||||
|
||||
/// Ringbuffer implementation based on `RingBufferTransient`
|
||||
impl<Key, Value, Index, B, CurrentIndex, Intermediate, M, QueryKind>
|
||||
RingBufferMap<Key, Value, QueryKind>
|
||||
for RingBufferMapImpl<Index, B, CurrentIndex, Intermediate, M, QueryKind>
|
||||
where
|
||||
Key: FullCodec + Clone,
|
||||
Value: FullCodec,
|
||||
Index: Ord + One + Zero + Add<Output = Index> + Copy + FullCodec + Eq,
|
||||
B: Get<Index>,
|
||||
CurrentIndex: StorageValue<Index, Query = Index>,
|
||||
Intermediate: StorageMap<Index, Key, Query = Key>,
|
||||
M: StorageMap<Key, Value, Query = QueryKind::Query>,
|
||||
QueryKind: QueryKindTrait<Value, GetDefault>,
|
||||
{
|
||||
/// Insert a map entry.
|
||||
fn insert(k: Key, v: Value) {
|
||||
let bound = B::get();
|
||||
let mut current_index = CurrentIndex::get();
|
||||
|
||||
// Adding one here as bound denotes number of items but our index starts with zero.
|
||||
if (current_index + Index::one()) >= bound {
|
||||
current_index = Index::zero();
|
||||
} else {
|
||||
current_index = current_index + Index::one();
|
||||
}
|
||||
|
||||
// Deleting earlier entry if it exists
|
||||
if Intermediate::contains_key(current_index) {
|
||||
let older_key = Intermediate::get(current_index);
|
||||
M::remove(older_key);
|
||||
}
|
||||
|
||||
Intermediate::insert(current_index, k.clone());
|
||||
CurrentIndex::set(current_index);
|
||||
M::insert(k, v);
|
||||
}
|
||||
|
||||
/// Check if map contains a key
|
||||
fn contains_key(k: Key) -> bool {
|
||||
M::contains_key(k)
|
||||
}
|
||||
|
||||
/// Get the value associated with key
|
||||
fn get(k: Key) -> M::Query {
|
||||
M::get(k)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use crate::{ChannelId, ParaId};
|
||||
use hex_literal::hex;
|
||||
|
||||
const EXPECT_CHANNEL_ID: [u8; 32] =
|
||||
hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539");
|
||||
|
||||
// The Solidity equivalent code is tested in Gateway.t.sol:testDeriveChannelID
|
||||
#[test]
|
||||
fn generate_channel_id() {
|
||||
let para_id: ParaId = 1000.into();
|
||||
let channel_id: ChannelId = para_id.into();
|
||||
assert_eq!(channel_id, EXPECT_CHANNEL_ID.into());
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use frame_support::traits::Contains;
|
||||
use snowbridge_core::AllowSiblingsOnly;
|
||||
use xcm::prelude::{Junction::Parachain, Junctions::X1, MultiLocation};
|
||||
|
||||
#[test]
|
||||
fn allow_siblings_predicate_only_allows_siblings() {
|
||||
let sibling = MultiLocation::new(1, X1(Parachain(1000)));
|
||||
let child = MultiLocation::new(0, X1(Parachain(1000)));
|
||||
assert!(AllowSiblingsOnly::contains(&sibling), "Sibling returns true.");
|
||||
assert!(!AllowSiblingsOnly::contains(&child), "Child returns false.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[target.wasm32-unknown-unknown]
|
||||
runner = 'wasm-bindgen-test-runner'
|
||||
@@ -0,0 +1,51 @@
|
||||
[package]
|
||||
name = "snowbridge-ethereum"
|
||||
description = "Snowbridge Ethereum"
|
||||
version = "0.1.0"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.188", optional = true, features = ["derive"] }
|
||||
serde-big-array = { version = "0.3.2", optional = true, features = ["const-generics"] }
|
||||
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = ["derive"] }
|
||||
scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
|
||||
ethbloom = { version = "0.13.0", default-features = false }
|
||||
ethereum-types = { version = "0.14.1", default-features = false, features = ["codec", "rlp", "serialize"] }
|
||||
hex = { package = "rustc-hex", version = "2.1.0", default-features = false }
|
||||
hex-literal = { version = "0.4.1", default-features = false }
|
||||
parity-bytes = { version = "0.1.2", default-features = false }
|
||||
rlp = { version = "0.5.2", default-features = false }
|
||||
|
||||
sp-io = { path = "../../../../../substrate/primitives/io", default-features = false }
|
||||
sp-std = { path = "../../../../../substrate/primitives/std", default-features = false }
|
||||
sp-core = { path = "../../../../../substrate/primitives/core", default-features = false }
|
||||
sp-runtime = { path = "../../../../../substrate/primitives/runtime", default-features = false }
|
||||
|
||||
ethabi = { git = "https://github.com/snowfork/ethabi-decode.git", package = "ethabi-decode", branch = "master", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.19"
|
||||
rand = "0.8.5"
|
||||
serde_json = "1.0.96"
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
expensive_tests = []
|
||||
std = [
|
||||
"codec/std",
|
||||
"ethabi/std",
|
||||
"ethbloom/std",
|
||||
"ethereum-types/std",
|
||||
"hex/std",
|
||||
"parity-bytes/std",
|
||||
"rlp/std",
|
||||
"scale-info/std",
|
||||
"serde",
|
||||
"serde-big-array",
|
||||
"sp-core/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
]
|
||||
@@ -0,0 +1,414 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use codec::{Decode, Encode};
|
||||
use ethbloom::Bloom as EthBloom;
|
||||
use hex_literal::hex;
|
||||
use parity_bytes::Bytes;
|
||||
use rlp::RlpStream;
|
||||
use scale_info::TypeInfo;
|
||||
use sp_io::hashing::keccak_256;
|
||||
use sp_runtime::RuntimeDebug;
|
||||
use sp_std::{convert::TryInto, prelude::*};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use serde_big_array::BigArray;
|
||||
|
||||
use ethereum_types::{Address, H256, H64, U256};
|
||||
|
||||
use crate::{mpt, receipt};
|
||||
|
||||
/// Complete block header id.
|
||||
#[derive(Clone, Copy, Default, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct HeaderId {
|
||||
/// Header number.
|
||||
pub number: u64,
|
||||
/// Header hash.
|
||||
pub hash: H256,
|
||||
}
|
||||
|
||||
const EMPTY_OMMERS_HASH: [u8; 32] =
|
||||
hex!("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347");
|
||||
|
||||
/// An Ethereum block header.
|
||||
#[derive(Clone, Default, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||
pub struct Header {
|
||||
/// Parent block hash.
|
||||
pub parent_hash: H256,
|
||||
/// Block timestamp.
|
||||
pub timestamp: u64,
|
||||
/// Block number.
|
||||
pub number: u64,
|
||||
/// Block author.
|
||||
pub author: Address,
|
||||
|
||||
/// Transactions root.
|
||||
pub transactions_root: H256,
|
||||
/// Block ommers hash.
|
||||
pub ommers_hash: H256,
|
||||
/// Block extra data.
|
||||
pub extra_data: Bytes,
|
||||
|
||||
/// State root.
|
||||
pub state_root: H256,
|
||||
/// Block receipts root.
|
||||
pub receipts_root: H256,
|
||||
/// Block bloom.
|
||||
pub logs_bloom: Bloom,
|
||||
/// Gas used for contracts execution.
|
||||
pub gas_used: U256,
|
||||
/// Block gas limit.
|
||||
pub gas_limit: U256,
|
||||
|
||||
/// Block difficulty.
|
||||
pub difficulty: U256,
|
||||
/// Vector of post-RLP-encoded fields.
|
||||
pub seal: Vec<Bytes>,
|
||||
|
||||
// Base fee per gas (EIP-1559), only in headers from the London hardfork onwards.
|
||||
pub base_fee: Option<U256>,
|
||||
}
|
||||
|
||||
impl Header {
|
||||
/// Compute hash of this header (keccak of the RLP with seal).
|
||||
pub fn compute_hash(&self) -> H256 {
|
||||
keccak_256(&self.rlp(true)).into()
|
||||
}
|
||||
|
||||
/// Compute hash of the truncated header i.e. excluding seal.
|
||||
pub fn compute_partial_hash(&self) -> H256 {
|
||||
keccak_256(&self.rlp(false)).into()
|
||||
}
|
||||
|
||||
pub fn check_receipt_proof(
|
||||
&self,
|
||||
proof: &[Vec<u8>],
|
||||
) -> Option<Result<receipt::Receipt, rlp::DecoderError>> {
|
||||
match self.apply_merkle_proof(proof) {
|
||||
Some((root, data)) if root == self.receipts_root => Some(rlp::decode(&data)),
|
||||
Some((_, _)) => None,
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_merkle_proof(&self, proof: &[Vec<u8>]) -> Option<(H256, Vec<u8>)> {
|
||||
let mut iter = proof.iter().rev();
|
||||
let first_bytes = match iter.next() {
|
||||
Some(b) => b,
|
||||
None => return None,
|
||||
};
|
||||
let item_to_prove: mpt::ShortNode = rlp::decode(first_bytes).ok()?;
|
||||
|
||||
let final_hash: Option<[u8; 32]> = iter.try_fold(keccak_256(first_bytes), |acc, x| {
|
||||
let node: Box<dyn mpt::Node> = x.as_slice().try_into().ok()?;
|
||||
if (*node).contains_hash(acc.into()) {
|
||||
return Some(keccak_256(x))
|
||||
}
|
||||
None
|
||||
});
|
||||
|
||||
final_hash.map(|hash| (hash.into(), item_to_prove.value))
|
||||
}
|
||||
|
||||
pub fn mix_hash(&self) -> Option<H256> {
|
||||
let bytes: Bytes = self.decoded_seal_field(0, 32)?;
|
||||
let size = bytes.len();
|
||||
let mut mix_hash = [0u8; 32];
|
||||
for i in 0..size {
|
||||
mix_hash[31 - i] = bytes[size - 1 - i];
|
||||
}
|
||||
Some(mix_hash.into())
|
||||
}
|
||||
|
||||
pub fn nonce(&self) -> Option<H64> {
|
||||
let bytes: Bytes = self.decoded_seal_field(1, 8)?;
|
||||
let size = bytes.len();
|
||||
let mut nonce = [0u8; 8];
|
||||
for i in 0..size {
|
||||
nonce[7 - i] = bytes[size - 1 - i];
|
||||
}
|
||||
Some(nonce.into())
|
||||
}
|
||||
|
||||
pub fn has_ommers(&self) -> bool {
|
||||
self.ommers_hash != EMPTY_OMMERS_HASH.into()
|
||||
}
|
||||
|
||||
fn decoded_seal_field(&self, index: usize, max_len: usize) -> Option<Bytes> {
|
||||
let bytes: Bytes = rlp::decode(self.seal.get(index)?).ok()?;
|
||||
if bytes.len() > max_len {
|
||||
return None
|
||||
}
|
||||
Some(bytes)
|
||||
}
|
||||
|
||||
/// Returns header RLP with or without seals.
|
||||
/// For EIP-1559 baseFee addition refer to:
|
||||
/// <https://github.com/openethereum/openethereum/blob/193b25a22d5ff07759c6431129e95235510516f9/crates/ethcore/types/src/header.rs#L341>
|
||||
fn rlp(&self, with_seal: bool) -> Bytes {
|
||||
let mut s = RlpStream::new();
|
||||
|
||||
let stream_length_without_seal = if self.base_fee.is_some() { 14 } else { 13 };
|
||||
|
||||
if with_seal {
|
||||
s.begin_list(stream_length_without_seal + self.seal.len());
|
||||
} else {
|
||||
s.begin_list(stream_length_without_seal);
|
||||
}
|
||||
|
||||
s.append(&self.parent_hash);
|
||||
s.append(&self.ommers_hash);
|
||||
s.append(&self.author);
|
||||
s.append(&self.state_root);
|
||||
s.append(&self.transactions_root);
|
||||
s.append(&self.receipts_root);
|
||||
s.append(&EthBloom::from(self.logs_bloom.0));
|
||||
s.append(&self.difficulty);
|
||||
s.append(&self.number);
|
||||
s.append(&self.gas_limit);
|
||||
s.append(&self.gas_used);
|
||||
s.append(&self.timestamp);
|
||||
s.append(&self.extra_data);
|
||||
|
||||
if with_seal {
|
||||
for b in &self.seal {
|
||||
s.append_raw(b, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(base_fee) = self.base_fee {
|
||||
s.append(&base_fee);
|
||||
}
|
||||
|
||||
s.out().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
/// Logs bloom.
|
||||
#[derive(Clone, Debug, Encode, Decode, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||
pub struct Bloom(#[cfg_attr(feature = "std", serde(with = "BigArray"))] [u8; 256]);
|
||||
|
||||
impl<'a> From<&'a [u8; 256]> for Bloom {
|
||||
fn from(buffer: &'a [u8; 256]) -> Bloom {
|
||||
Bloom(*buffer)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Bloom> for Bloom {
|
||||
fn eq(&self, other: &Bloom) -> bool {
|
||||
self.0.iter().zip(other.0.iter()).all(|(l, r)| l == r)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Bloom {
|
||||
fn default() -> Self {
|
||||
Bloom([0; 256])
|
||||
}
|
||||
}
|
||||
|
||||
impl rlp::Decodable for Bloom {
|
||||
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
|
||||
let v: Vec<u8> = rlp.as_val()?;
|
||||
match v.len() {
|
||||
256 => {
|
||||
let mut bytes = [0u8; 256];
|
||||
bytes.copy_from_slice(&v);
|
||||
Ok(Self(bytes))
|
||||
},
|
||||
_ => Err(rlp::DecoderError::Custom("Expected 256 bytes")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bloom_decode_rlp() {
|
||||
let raw_bloom = hex!(
|
||||
"
|
||||
b901000420000000000000000000008002000000000001000000000001000000000000000000
|
||||
0000000000000000000000000002000000080000000000000000200000000000000000000000
|
||||
0000080000002200000000004000100000000000000000000000000000000000000000000000
|
||||
0000000000000004000000001000010000000000080000000000400000000000000000000000
|
||||
0000080000004000000000020000000000020000000000000000000000000000000000000000
|
||||
0000040000000000020000000001000000000000000000000000000010000000020000200000
|
||||
10200000000000010000000000000000000000000000000000000010000000
|
||||
"
|
||||
);
|
||||
let expected_bytes = &raw_bloom[3..];
|
||||
let bloom: Bloom = rlp::decode(&raw_bloom).unwrap();
|
||||
assert_eq!(bloom.0, expected_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_compute_hash_poa() {
|
||||
// PoA header
|
||||
let header = Header {
|
||||
parent_hash: Default::default(),
|
||||
timestamp: 0,
|
||||
number: 0,
|
||||
author: Default::default(),
|
||||
transactions_root: hex!(
|
||||
"56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"
|
||||
)
|
||||
.into(),
|
||||
ommers_hash: hex!("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347")
|
||||
.into(),
|
||||
extra_data: vec![],
|
||||
state_root: hex!("eccf6b74c2bcbe115c71116a23fe963c54406010c244d9650526028ad3e32cce")
|
||||
.into(),
|
||||
receipts_root: hex!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")
|
||||
.into(),
|
||||
logs_bloom: Default::default(),
|
||||
gas_used: Default::default(),
|
||||
gas_limit: 0x222222.into(),
|
||||
difficulty: 0x20000.into(),
|
||||
seal: vec![vec![0x80], {
|
||||
let mut vec = vec![0xb8, 0x41];
|
||||
vec.resize(67, 0);
|
||||
vec
|
||||
}],
|
||||
base_fee: None,
|
||||
};
|
||||
assert_eq!(
|
||||
header.compute_hash().as_bytes(),
|
||||
hex!("9ff57c7fa155853586382022f0982b71c51fa313a0942f8c456300896643e890"),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_compute_hash_pow() {
|
||||
// <https://etherscan.io/block/11090290>
|
||||
let nonce = hex!("6935bbe7b63c4f8e").to_vec();
|
||||
let mix_hash =
|
||||
hex!("be3adfb0087be62b28b716e2cdf3c79329df5caa04c9eee035d35b5d52102815").to_vec();
|
||||
let header = Header {
|
||||
parent_hash: hex!("bede0bddd6f32c895fc505ffe0c39d9bde58e9a5272f31a3dee448b796edcbe3")
|
||||
.into(),
|
||||
timestamp: 1603160977,
|
||||
number: 11090290,
|
||||
author: hex!("ea674fdde714fd979de3edf0f56aa9716b898ec8").into(),
|
||||
transactions_root: hex!(
|
||||
"56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"
|
||||
)
|
||||
.into(),
|
||||
ommers_hash: hex!("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347")
|
||||
.into(),
|
||||
extra_data: hex!("65746865726d696e652d61736961312d33").to_vec(),
|
||||
state_root: hex!("7dcb8aca872b712bad81df34a89d4efedc293566ffc3eeeb5cbcafcc703e42c9")
|
||||
.into(),
|
||||
receipts_root: hex!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")
|
||||
.into(),
|
||||
logs_bloom: Default::default(),
|
||||
gas_used: 0.into(),
|
||||
gas_limit: 0xbe8c19.into(),
|
||||
difficulty: 0xbc140caa61087i64.into(),
|
||||
seal: vec![rlp::encode(&mix_hash).to_vec(), rlp::encode(&nonce).to_vec()],
|
||||
base_fee: None,
|
||||
};
|
||||
assert_eq!(
|
||||
header.compute_hash().as_bytes(),
|
||||
hex!("0f9bdc91c2e0140acb873330742bda8c8181fa3add91fe7ae046251679cedef7"),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_pow_seal_fields_extracted_correctly() {
|
||||
let nonce: H64 = hex!("6935bbe7b63c4f8e").into();
|
||||
let mix_hash: H256 =
|
||||
hex!("be3adfb0087be62b28b716e2cdf3c79329df5caa04c9eee035d35b5d52102815").into();
|
||||
let header = Header {
|
||||
seal: vec![
|
||||
rlp::encode(&mix_hash.0.to_vec()).to_vec(),
|
||||
rlp::encode(&nonce.0.to_vec()).to_vec(),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(header.nonce().unwrap(), nonce);
|
||||
assert_eq!(header.mix_hash().unwrap(), mix_hash);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_pow_seal_fields_return_none_for_invalid_values() {
|
||||
let nonce = hex!("696935bbe7b63c4f8e").to_vec();
|
||||
let mix_hash =
|
||||
hex!("bebe3adfb0087be62b28b716e2cdf3c79329df5caa04c9eee035d35b5d52102815").to_vec();
|
||||
let mut header = Header {
|
||||
seal: vec![rlp::encode(&mix_hash).to_vec(), rlp::encode(&nonce).to_vec()],
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(header.nonce(), None);
|
||||
assert_eq!(header.mix_hash(), None);
|
||||
|
||||
header.seal = Vec::new();
|
||||
assert_eq!(header.nonce(), None);
|
||||
assert_eq!(header.mix_hash(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_check_receipt_proof() {
|
||||
let header = Header {
|
||||
receipts_root: hex!("fd5e397a84884641f53c496804f24b5276cbb8c5c9cfc2342246be8e3ce5ad02")
|
||||
.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Valid proof
|
||||
let proof_receipt5 = vec!(
|
||||
hex!("f90131a0b5ba404eb5a6a88e56579f4d37ef9813b5ad7f86f0823ff3b407ac5a6bb465eca0398ead2655e78e03c127ce22c5830e90f18b1601ec055f938336c084feb915a9a026d322c26e46c50942c1aabde50e36df5cde572aed650ce73ea3182c6e90a02ca00600a356135f4db1db0d9842264cdff2652676f881669e91e316c0b6dd783011a0837f1deb4075336da320388c1edfffc56c448a43f4a5ba031300d32a7b509fc5a01c3ac82fd65b4aba7f9afaf604d9c82ec7e2deb573a091ae235751bc5c0c288da05d454159d9071b0f68b6e0503d290f23ac7602c1db0c569dee4605d8f5298f09a00bbed10350ec954448df795f6fd46e3faefc800ede061b3840eedc6e2b07a74da0acb02d26a3650f2064c14a435fdf1f668d8655daf455ebdf671713a7c089b3898080808080808080").to_vec(),
|
||||
hex!("f901f180a00046a08d4f0bdbdc6b31903086ce323182bce6725e7d9415f7ff91ee8f4820bda0e7cd26ad5f3d2771e4b5ab788e268a14a10209f94ee918eb6c829d21d3d11c1da00d4a56d9e9a6751874fd86c7e3cb1c6ad5a848da62751325f478978a00ea966ea064b81920c8f04a8a1e21f53a8280e739fbb7b00b2ab92493ca3f610b70e8ac85a0b1040ed4c55a73178b76abb16f946ce5bebd6b93ab873c83327df54047d12c27a0de6485e9ac58dc6e2b04b4bb38f562684f0b1a2ee586cc11079e7d9a9dc40b32a0d394f4d3532c3124a65fa36e69147e04fd20453a72ee9c50660f17e13ce9df48a066501003fc3e3478efd2803cd0eded6bbe9243ca01ba754d6327071ddbcbc649a0b2684e518f325fee39fc8ea81b68f3f5c785be00d087f3bed8857ae2ee8da26ea071060a5c52042e8d7ce21092f8ecf06053beb9a0b773a6f91a30c4220aa276b2a0fc22436632574ccf6043d0986dede27ea94c9ca9a3bb5ec03ce776a4ddef24a9a05a8a1d6698c4e7d8cc3a2506cb9b12ea9a079c9c7099bc919dc804033cc556e4a0170c468b0716fd36d161f0bf05875f15756a2976de92f9efe7716320509d79c9a0182f909a90cab169f3efb62387f9cccdd61440acc4deec42f68a4f7ca58075c7a055cf0e9202ac75689b76318f1171f3a44465eddc06aae0713bfb6b34fdd27b7980").to_vec(),
|
||||
hex!("f904de20b904daf904d701830652f0b9010004200000000000000000000080020000000000010000000000010000000000000000000000000000000000000000000002000000080000000000000000200000000000000000000000000008000000220000000000400010000000000000000000000000000000000000000000000000000000000000040000000010000100000000000800000000004000000000000000000000000000080000004000000000020000000000020000000000000000000000000000000000000000000004000000000002000000000100000000000000000000000000001000000002000020000010200000000000010000000000000000000000000000000000000010000000f903ccf89b9421130f34829b4c343142047a28ce96ec07814b15f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000007d843005c7433c16b27ff939cb37471541561ebda0000000000000000000000000e9c1281aae66801fa35ec404d5f2aea393ff6988a000000000000000000000000000000000000000000000000000000005d09b7380f89b9421130f34829b4c343142047a28ce96ec07814b15f863a08c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925a00000000000000000000000007d843005c7433c16b27ff939cb37471541561ebda00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da0ffffffffffffffffffffffffffffffffffffffffffffffffffffffcc840c6920f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa0000000000000000000000000e9c1281aae66801fa35ec404d5f2aea393ff6988a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da000000000000000000000000000000000000000000000000003e973b5a5d1078ef87994e9c1281aae66801fa35ec404d5f2aea393ff6988e1a01c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1b840000000000000000000000000000000000000000000000000000001f1420ad1d40000000000000000000000000000000000000000000000014ad400879d159a38f8fc94e9c1281aae66801fa35ec404d5f2aea393ff6988f863a0d78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488db88000000000000000000000000000000000000000000000000000000005d415f3320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e973b5a5d1078ef87a94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a07fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da000000000000000000000000000000000000000000000000003e973b5a5d1078e").to_vec(),
|
||||
);
|
||||
assert!(header.check_receipt_proof(&proof_receipt5).is_some());
|
||||
|
||||
// Various invalid proofs
|
||||
let proof_empty: Vec<Vec<u8>> = vec![];
|
||||
let proof_missing_full_node = vec![proof_receipt5[0].clone(), proof_receipt5[2].clone()];
|
||||
let proof_missing_short_node1 = vec![proof_receipt5[0].clone(), proof_receipt5[1].clone()];
|
||||
let proof_missing_short_node2 = vec![proof_receipt5[0].clone()];
|
||||
let proof_invalid_encoding = vec![proof_receipt5[2][2..].to_vec()];
|
||||
let proof_no_full_node = vec![proof_receipt5[2].clone(), proof_receipt5[2].clone()];
|
||||
assert!(header.check_receipt_proof(&proof_empty).is_none());
|
||||
assert!(header.check_receipt_proof(&proof_missing_full_node).is_none());
|
||||
|
||||
assert_eq!(
|
||||
header.check_receipt_proof(&proof_missing_short_node1),
|
||||
Some(Err(rlp::DecoderError::Custom("Unsupported receipt type")))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
header.check_receipt_proof(&proof_missing_short_node2),
|
||||
Some(Err(rlp::DecoderError::Custom("Unsupported receipt type")))
|
||||
);
|
||||
|
||||
assert!(header.check_receipt_proof(&proof_invalid_encoding).is_none());
|
||||
assert!(header.check_receipt_proof(&proof_no_full_node).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_check_receipt_proof_with_intermediate_short_node() {
|
||||
let header = Header {
|
||||
receipts_root: hex!("d128e3a57142d2bf15bc0cbcac7ad54f40750d571b5c3097e425882c10c9ba66")
|
||||
.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let proof_receipt263 = vec![
|
||||
hex!("f90131a00d3cb8d3f57ac1c0e12918a2ebe0cafed8c273577b9dd73e7ed1079b403ef494a0678b9835b834f8a287c0dd33a8fca9146e456ca688555ed4ec1361a2180b778da0fe42da181a46677a043b3d9d4b8bb05a6a17b7b5c010c17e7c1d31cfb7c4f911a0c89f0e2c53241cdb578e1f2b4caf6ba36e00500bdc57fecd66b84a6a58394c19a086c3c1fae5a0575940b5d38e111c469d07883106c26856f3ef608469a2081f13a06c5992ff00aab6226a70a032fd2f571ba22f797321f45e2daa73020d638d21b0a050861e9503ef68728f6c90a44f7fe1bceb2a9bdab6957bbe7136166bd849561ea006aa6eaca8a07e57176e9aa41e6a09edfb7678d1a112404e0ec779d7e567e82ea0bb0b430d303ba21b0af11c487b8a218bd75db54c98940b3f11bad8ff47cad3ef8080808080808080").to_vec(),
|
||||
hex!("f871a0246de222036ee6a03329b0105da0a6b3f916fc95a9ed5a403a581a0c4d74242ca0ac108a49a88b57a05ac34a108b39f1e45f6f167f2b9fbc8d52fb58e2e5a6af1ea0fcfe07ac2ccd3c28b6eab68d1bce112f6f6dbd9023e4ec3c05b96615aa803d798080808080808080808080808080").to_vec(),
|
||||
hex!("e4820001a04fff54398cad4d05ea6abfd8b0f3b4fe14c04d7ff5f5211c5b927d9cf72ac1d8").to_vec(),
|
||||
hex!("f851a096d010643ca2d47412ca66898286b5f2412963b9ec051b33e570d575914c9c5ca028cd24c652989542fe89479ec6388eac4592432242af5ba97563b3ac7c71c019808080808080808080808080808080").to_vec(),
|
||||
hex!("f90211a0bb35a84c5b1dcb78ec9d32614912c696e62df77bebf9ab326ee55b5d3acdde46a01084b30dac8df0accfcd0fd6330b7f6fc72a4651246d0694be9162151686a620a03eed50afdce7909d784c6157c445a444c806b5f23d31f3b63786f600c84a95b2a0af5232f1df6c6d41879804d081abe867002abe26ba3e5f8e0254a83a54769831a0607915fb13dd5da594256389a45007a67a7f7a86e95d38d8462792b6c98a722ea00e1260fda1730f2738c650ce2bfba83857bc10f8fb119ebc4fb39acba24e6fbaa0d11de17e417327457812675ca3b84ae8e1b64827abfe01420953697c8313d5b1a05fcaf2f7a88f76336a0c32ffc78acb87ae2005454bd25d658035331be3173b46a03f94f4952ab9e650f83cfd0e7f367b1bcc493aacf39a06f16c4a2e1b5605da48a0bdb4ec79785ca8ae22d60f1bbd42d707b4d7ec4aff231a3ebab755e315b35053a043a67c3f2bcef37c8f47a673adcb7061007a553696d1092408601c11b2e6846aa0c519d5af48cae87c7f4538845417c9735813bee892a6fe2dda79f5c414e8576aa0f7058256e09589501d7c231d739e61c84a850e139690989d24fda6058b432e98a081a52faab520978cb19ce14400dba0cd5bcdc4e5a3c0740678aa8f97ee0e5c56a0bcecc61cadeae52518e3b68a48af4b11603dfd9d99d99d7985efa6d2de44f904a02cba4accfc6f39bc5adb6d4440eb6358b4a5103ef93298e4e694f1f940f8b48280").to_vec(),
|
||||
hex!("f901ae20b901aaf901a70183bb444eb9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000001000000000000000000000000000100000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000010000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000080000000000000000000000000000000000000000000000002000000000000000000081000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000f89df89b94dac17f958d2ee523a2206206994597c13d831ec7f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000002e514404ff6823f1b46a8318a709251db414e5e1a000000000000000000000000055021c55847c00d764357a352e5803237d328954a0000000000000000000000000000000000000000000000000000000000201c370").to_vec(),
|
||||
];
|
||||
assert!(header.check_receipt_proof(&proof_receipt263).is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
pub mod header;
|
||||
pub mod log;
|
||||
pub mod mpt;
|
||||
pub mod receipt;
|
||||
|
||||
pub use ethereum_types::{Address, H160, H256, H64, U256};
|
||||
|
||||
pub use header::{Bloom, Header, HeaderId};
|
||||
pub use log::Log;
|
||||
pub use receipt::Receipt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DecodeError {
|
||||
// Unexpected RLP data
|
||||
InvalidRLP(rlp::DecoderError),
|
||||
// Data does not match expected ABI
|
||||
InvalidABI(ethabi::Error),
|
||||
// Invalid message payload
|
||||
InvalidPayload,
|
||||
}
|
||||
|
||||
impl From<rlp::DecoderError> for DecodeError {
|
||||
fn from(err: rlp::DecoderError) -> Self {
|
||||
DecodeError::InvalidRLP(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ethabi::Error> for DecodeError {
|
||||
fn from(err: ethabi::Error) -> Self {
|
||||
DecodeError::InvalidABI(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use codec::{Decode, Encode};
|
||||
use ethereum_types::{H160, H256};
|
||||
use sp_std::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Encode, Decode, PartialEq, Eq)]
|
||||
pub struct Log {
|
||||
pub address: H160,
|
||||
pub topics: Vec<H256>,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl rlp::Decodable for Log {
|
||||
/// We need to implement rlp::Decodable manually as the derive macro RlpDecodable
|
||||
/// didn't seem to generate the correct code for parsing our logs.
|
||||
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
|
||||
let mut iter = rlp.iter();
|
||||
|
||||
let address: H160 = match iter.next() {
|
||||
Some(data) => data.as_val()?,
|
||||
None => return Err(rlp::DecoderError::Custom("Expected log address")),
|
||||
};
|
||||
|
||||
let topics: Vec<H256> = match iter.next() {
|
||||
Some(data) => data.as_list()?,
|
||||
None => return Err(rlp::DecoderError::Custom("Expected log topics")),
|
||||
};
|
||||
|
||||
let data: Vec<u8> = match iter.next() {
|
||||
Some(data) => data.data()?.to_vec(),
|
||||
None => return Err(rlp::DecoderError::Custom("Expected log data")),
|
||||
};
|
||||
|
||||
Ok(Self { address, topics, data })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::Log;
|
||||
use hex_literal::hex;
|
||||
|
||||
const RAW_LOG: [u8; 605] = hex!(
|
||||
"
|
||||
f9025a941cfd66659d44cfe2e627c5742ba7477a3284cffae1a0266413be5700ce8dd5ac6b9a7dfb
|
||||
abe99b3e45cae9a68ac2757858710b401a38b9022000000000000000000000000000000000000000
|
||||
00000000000000000000000060000000000000000000000000000000000000000000000000000000
|
||||
00000000c00000000000000000000000000000000000000000000000000000000000000100000000
|
||||
00000000000000000000000000000000000000000000000000000000283163466436363635394434
|
||||
34636665324536323763353734324261373437376133323834634666410000000000000000000000
|
||||
00000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
000000000773656e6445544800000000000000000000000000000000000000000000000000000000
|
||||
00000000000000000000000000000000000000000000000000000001000000000000000000000000
|
||||
00cffeaaf7681c89285d65cfbe808b80e50269657300000000000000000000000000000000000000
|
||||
000000000000000000000000a0000000000000000000000000000000000000000000000000000000
|
||||
0000000000000000000000000000000000000000000000000000000000000000000000000a000000
|
||||
00000000000000000000000000000000000000000000000000000000020000000000000000000000
|
||||
00000000000000000000000000000000000000002f3146524d4d3850456957585961783772705336
|
||||
5834585a5831614141785357783143724b5479725659685632346667000000000000000000000000
|
||||
0000000000
|
||||
"
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn decode_log() {
|
||||
let log: Log = rlp::decode(&RAW_LOG).unwrap();
|
||||
assert_eq!(log.address.as_bytes(), hex!["1cfd66659d44cfe2e627c5742ba7477a3284cffa"]);
|
||||
assert_eq!(
|
||||
log.topics[0].as_bytes(),
|
||||
hex!["266413be5700ce8dd5ac6b9a7dfbabe99b3e45cae9a68ac2757858710b401a38"]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Helper types to work with Ethereum's Merkle Patricia Trie nodes
|
||||
|
||||
use ethereum_types::H256;
|
||||
use sp_std::{convert::TryFrom, prelude::*};
|
||||
|
||||
pub trait Node {
|
||||
fn contains_hash(&self, hash: H256) -> bool;
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Box<dyn Node> {
|
||||
type Error = rlp::DecoderError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Box<dyn Node>, Self::Error> {
|
||||
let rlp = rlp::Rlp::new(bytes);
|
||||
match rlp.item_count()? {
|
||||
2 => {
|
||||
let node: ShortNode = rlp.as_val()?;
|
||||
Ok(Box::new(node))
|
||||
},
|
||||
17 => {
|
||||
let node: FullNode = rlp.as_val()?;
|
||||
Ok(Box::new(node))
|
||||
},
|
||||
_ => Err(rlp::DecoderError::Custom("Invalid number of list elements")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Intermediate trie node with children (refers to node with same name in Geth).
|
||||
/// This struct only handles the proof representation, i.e. a child is either empty
|
||||
/// or a 32-byte hash of its subtree.
|
||||
pub struct FullNode {
|
||||
pub children: Vec<Option<H256>>,
|
||||
}
|
||||
|
||||
impl rlp::Decodable for FullNode {
|
||||
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
|
||||
let children: Vec<Option<H256>> = rlp
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let v: Vec<u8> = item.as_val()?;
|
||||
match v.len() {
|
||||
0 => Ok(None),
|
||||
32 => {
|
||||
let mut bytes = [0u8; 32];
|
||||
bytes.copy_from_slice(&v);
|
||||
Ok(Some(bytes.into()))
|
||||
},
|
||||
_ => Err(rlp::DecoderError::Custom("Expected 32-byte hash or empty child")),
|
||||
}
|
||||
})
|
||||
.collect::<Result<_, rlp::DecoderError>>()?;
|
||||
|
||||
Ok(Self { children })
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for FullNode {
|
||||
fn contains_hash(&self, hash: H256) -> bool {
|
||||
self.children.iter().any(|h| Some(hash) == *h)
|
||||
}
|
||||
}
|
||||
|
||||
/// Trie node where `value` is either the RLP-encoded item we're
|
||||
/// proving or an intermediate hash (refers to node with same name in Geth)
|
||||
/// Proof verification should return `value`. `key` is an implementation
|
||||
/// detail of the trie.
|
||||
pub struct ShortNode {
|
||||
pub key: Vec<u8>,
|
||||
pub value: Vec<u8>,
|
||||
}
|
||||
|
||||
impl rlp::Decodable for ShortNode {
|
||||
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
|
||||
let mut iter = rlp.iter();
|
||||
|
||||
let key: Vec<u8> = match iter.next() {
|
||||
Some(data) => data.as_val()?,
|
||||
None => return Err(rlp::DecoderError::Custom("Expected key bytes")),
|
||||
};
|
||||
|
||||
let value: Vec<u8> = match iter.next() {
|
||||
Some(data) => data.as_val()?,
|
||||
None => return Err(rlp::DecoderError::Custom("Expected value bytes")),
|
||||
};
|
||||
|
||||
Ok(Self { key, value })
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for ShortNode {
|
||||
fn contains_hash(&self, hash: H256) -> bool {
|
||||
self.value == hash.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use hex_literal::hex;
|
||||
|
||||
const RAW_PROOF: [&[u8]; 3] = [
|
||||
&hex!("f90131a0b5ba404eb5a6a88e56579f4d37ef9813b5ad7f86f0823ff3b407ac5a6bb465eca0398ead2655e78e03c127ce22c5830e90f18b1601ec055f938336c084feb915a9a026d322c26e46c50942c1aabde50e36df5cde572aed650ce73ea3182c6e90a02ca00600a356135f4db1db0d9842264cdff2652676f881669e91e316c0b6dd783011a0837f1deb4075336da320388c1edfffc56c448a43f4a5ba031300d32a7b509fc5a01c3ac82fd65b4aba7f9afaf604d9c82ec7e2deb573a091ae235751bc5c0c288da05d454159d9071b0f68b6e0503d290f23ac7602c1db0c569dee4605d8f5298f09a00bbed10350ec954448df795f6fd46e3faefc800ede061b3840eedc6e2b07a74da0acb02d26a3650f2064c14a435fdf1f668d8655daf455ebdf671713a7c089b3898080808080808080"),
|
||||
&hex!("f901f180a00046a08d4f0bdbdc6b31903086ce323182bce6725e7d9415f7ff91ee8f4820bda0e7cd26ad5f3d2771e4b5ab788e268a14a10209f94ee918eb6c829d21d3d11c1da00d4a56d9e9a6751874fd86c7e3cb1c6ad5a848da62751325f478978a00ea966ea064b81920c8f04a8a1e21f53a8280e739fbb7b00b2ab92493ca3f610b70e8ac85a0b1040ed4c55a73178b76abb16f946ce5bebd6b93ab873c83327df54047d12c27a0de6485e9ac58dc6e2b04b4bb38f562684f0b1a2ee586cc11079e7d9a9dc40b32a0d394f4d3532c3124a65fa36e69147e04fd20453a72ee9c50660f17e13ce9df48a066501003fc3e3478efd2803cd0eded6bbe9243ca01ba754d6327071ddbcbc649a0b2684e518f325fee39fc8ea81b68f3f5c785be00d087f3bed8857ae2ee8da26ea071060a5c52042e8d7ce21092f8ecf06053beb9a0b773a6f91a30c4220aa276b2a0fc22436632574ccf6043d0986dede27ea94c9ca9a3bb5ec03ce776a4ddef24a9a05a8a1d6698c4e7d8cc3a2506cb9b12ea9a079c9c7099bc919dc804033cc556e4a0170c468b0716fd36d161f0bf05875f15756a2976de92f9efe7716320509d79c9a0182f909a90cab169f3efb62387f9cccdd61440acc4deec42f68a4f7ca58075c7a055cf0e9202ac75689b76318f1171f3a44465eddc06aae0713bfb6b34fdd27b7980"),
|
||||
&hex!("f904de20b904daf904d701830652f0b9010004200000000000000000000080020000000000010000000000010000000000000000000000000000000000000000000002000000080000000000000000200000000000000000000000000008000000220000000000400010000000000000000000000000000000000000000000000000000000000000040000000010000100000000000800000000004000000000000000000000000000080000004000000000020000000000020000000000000000000000000000000000000000000004000000000002000000000100000000000000000000000000001000000002000020000010200000000000010000000000000000000000000000000000000010000000f903ccf89b9421130f34829b4c343142047a28ce96ec07814b15f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000007d843005c7433c16b27ff939cb37471541561ebda0000000000000000000000000e9c1281aae66801fa35ec404d5f2aea393ff6988a000000000000000000000000000000000000000000000000000000005d09b7380f89b9421130f34829b4c343142047a28ce96ec07814b15f863a08c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925a00000000000000000000000007d843005c7433c16b27ff939cb37471541561ebda00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da0ffffffffffffffffffffffffffffffffffffffffffffffffffffffcc840c6920f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa0000000000000000000000000e9c1281aae66801fa35ec404d5f2aea393ff6988a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da000000000000000000000000000000000000000000000000003e973b5a5d1078ef87994e9c1281aae66801fa35ec404d5f2aea393ff6988e1a01c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1b840000000000000000000000000000000000000000000000000000001f1420ad1d40000000000000000000000000000000000000000000000014ad400879d159a38f8fc94e9c1281aae66801fa35ec404d5f2aea393ff6988f863a0d78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488db88000000000000000000000000000000000000000000000000000000005d415f3320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e973b5a5d1078ef87a94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a07fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da000000000000000000000000000000000000000000000000003e973b5a5d1078e"),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn decode_full_node() {
|
||||
let node1: FullNode = rlp::decode(RAW_PROOF[0]).unwrap();
|
||||
let node2: FullNode = rlp::decode(RAW_PROOF[1]).unwrap();
|
||||
assert_eq!(node1.children.len(), 17);
|
||||
assert_eq!(node2.children.len(), 17);
|
||||
assert_eq!(node1.children.iter().filter(|c| c.is_none()).count(), 8);
|
||||
assert_eq!(node2.children.iter().filter(|c| c.is_none()).count(), 2);
|
||||
|
||||
let result: Result<FullNode, rlp::DecoderError> = rlp::decode(RAW_PROOF[2]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_short_node() {
|
||||
// key + item value
|
||||
let node: ShortNode = rlp::decode(RAW_PROOF[2]).unwrap();
|
||||
assert_eq!(node.key, vec![32]);
|
||||
assert!(!node.value.is_empty());
|
||||
|
||||
// key + item hash
|
||||
let node: ShortNode = rlp::decode(&hex!(
|
||||
"e4820001a04fff54398cad4d05ea6abfd8b0f3b4fe14c04d7ff5f5211c5b927d9cf72ac1d8"
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(node.key, vec![0, 1]);
|
||||
assert_eq!(
|
||||
node.value,
|
||||
hex!("4fff54398cad4d05ea6abfd8b0f3b4fe14c04d7ff5f5211c5b927d9cf72ac1d8").to_vec()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use crate::{Bloom, Log};
|
||||
use codec::{Decode, Encode};
|
||||
use sp_runtime::RuntimeDebug;
|
||||
use sp_std::prelude::*;
|
||||
|
||||
#[derive(Clone, Default, Encode, Decode, PartialEq, RuntimeDebug)]
|
||||
pub struct Receipt {
|
||||
pub post_state_or_status: Vec<u8>,
|
||||
pub cumulative_gas_used: u64,
|
||||
pub bloom: Bloom,
|
||||
pub logs: Vec<Log>,
|
||||
}
|
||||
|
||||
impl Receipt {
|
||||
pub fn contains_log(&self, log: &Log) -> bool {
|
||||
self.logs.iter().any(|l| l == log)
|
||||
}
|
||||
|
||||
fn decode_list(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
|
||||
let mut iter = rlp.iter();
|
||||
|
||||
let post_state_or_status: Vec<u8> = match iter.next() {
|
||||
Some(data) => data.as_val()?,
|
||||
None => return Err(rlp::DecoderError::Custom("Expected receipt post state or status")),
|
||||
};
|
||||
|
||||
let cumulative_gas_used: u64 = match iter.next() {
|
||||
Some(data) => data.as_val()?,
|
||||
None => return Err(rlp::DecoderError::Custom("Expected receipt cumulative gas used")),
|
||||
};
|
||||
|
||||
let bloom: Bloom = match iter.next() {
|
||||
Some(data) => data.as_val()?,
|
||||
None => return Err(rlp::DecoderError::Custom("Expected receipt bloom")),
|
||||
};
|
||||
|
||||
let logs: Vec<Log> = match iter.next() {
|
||||
Some(data) => data.as_list()?,
|
||||
None => return Err(rlp::DecoderError::Custom("Expected receipt logs")),
|
||||
};
|
||||
|
||||
Ok(Self { post_state_or_status, cumulative_gas_used, bloom, logs })
|
||||
}
|
||||
}
|
||||
|
||||
impl rlp::Decodable for Receipt {
|
||||
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
|
||||
if rlp.is_data() {
|
||||
// Typed receipt
|
||||
let data = rlp.as_raw();
|
||||
match data[0] {
|
||||
// 1 = EIP-2930, 2 = EIP-1559
|
||||
1 | 2 => {
|
||||
let receipt_rlp = &rlp::Rlp::new(&data[1..]);
|
||||
if !receipt_rlp.is_list() {
|
||||
return Err(rlp::DecoderError::RlpExpectedToBeList)
|
||||
}
|
||||
Self::decode_list(&rlp::Rlp::new(&data[1..]))
|
||||
},
|
||||
_ => Err(rlp::DecoderError::Custom("Unsupported receipt type")),
|
||||
}
|
||||
} else if rlp.is_list() {
|
||||
// Legacy receipt
|
||||
Self::decode_list(rlp)
|
||||
} else {
|
||||
Err(rlp::DecoderError::RlpExpectedToBeList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::Receipt;
|
||||
use hex_literal::hex;
|
||||
|
||||
const RAW_RECEIPT: [u8; 1242] = hex!(
|
||||
"
|
||||
f904d701830652f0b901000420000000000000000000008002000000000001000000000001000000
|
||||
00000000000000000000000000000000000000020000000800000000000000002000000000000000
|
||||
00000000000008000000220000000000400010000000000000000000000000000000000000000000
|
||||
00000000000000000004000000001000010000000000080000000000400000000000000000000000
|
||||
00000800000040000000000200000000000200000000000000000000000000000000000000000000
|
||||
04000000000002000000000100000000000000000000000000001000000002000020000010200000
|
||||
000000010000000000000000000000000000000000000010000000f903ccf89b9421130f34829b4c
|
||||
343142047a28ce96ec07814b15f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a116
|
||||
28f55a4df523b3efa00000000000000000000000007d843005c7433c16b27ff939cb37471541561e
|
||||
bda0000000000000000000000000e9c1281aae66801fa35ec404d5f2aea393ff6988a00000000000
|
||||
0000000000000000000000000000000000000000000005d09b7380f89b9421130f34829b4c343142
|
||||
047a28ce96ec07814b15f863a08c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200a
|
||||
c8c7c3b925a00000000000000000000000007d843005c7433c16b27ff939cb37471541561ebda000
|
||||
00000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da0ffffffffffffffff
|
||||
ffffffffffffffffffffffffffffffffffffffcc840c6920f89b94c02aaa39b223fe8d0a0e5c4f27
|
||||
ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523
|
||||
b3efa0000000000000000000000000e9c1281aae66801fa35ec404d5f2aea393ff6988a000000000
|
||||
00000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da00000000000000000000000
|
||||
0000000000000000000000000003e973b5a5d1078ef87994e9c1281aae66801fa35ec404d5f2aea3
|
||||
93ff6988e1a01c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1b840
|
||||
000000000000000000000000000000000000000000000000000001f1420ad1d40000000000000000
|
||||
000000000000000000000000000000014ad400879d159a38f8fc94e9c1281aae66801fa35ec404d5
|
||||
f2aea393ff6988f863a0d78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159
|
||||
d822a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da000000000
|
||||
00000000000000007a250d5630b4cf539739df2c5dacb4c659f2488db88000000000000000000000
|
||||
000000000000000000000000000000000005d415f332000000000000000000000000000000000000
|
||||
00000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
00000000000000000000000000000000000000000000000000000000000003e973b5a5d1078ef87a
|
||||
94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a07fcf532c15f0a6db0bd6d0e038bea71d
|
||||
30d808c7d98cb3bf7268a95bf5081b65a00000000000000000000000007a250d5630b4cf539739df
|
||||
2c5dacb4c659f2488da000000000000000000000000000000000000000000000000003e973b5a5d1
|
||||
078e
|
||||
"
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn decode_legacy_receipt() {
|
||||
let receipt: Receipt = rlp::decode(&RAW_RECEIPT).unwrap();
|
||||
assert_eq!(receipt.post_state_or_status, vec!(1));
|
||||
assert_eq!(receipt.cumulative_gas_used, 414448);
|
||||
assert_eq!(
|
||||
receipt.bloom,
|
||||
(&hex!(
|
||||
"
|
||||
042000000000000000000000800200000000000100000000000100000000000000000000
|
||||
000000000000000000000000020000000800000000000000002000000000000000000000
|
||||
000000080000002200000000004000100000000000000000000000000000000000000000
|
||||
000000000000000000000400000000100001000000000008000000000040000000000000
|
||||
000000000000000800000040000000000200000000000200000000000000000000000000
|
||||
000000000000000000040000000000020000000001000000000000000000000000000010
|
||||
000000020000200000102000000000000100000000000000000000000000000000000000
|
||||
10000000
|
||||
"
|
||||
))
|
||||
.into(),
|
||||
);
|
||||
assert_eq!(receipt.logs.len(), 6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
[package]
|
||||
name = "snowbridge-router-primitives"
|
||||
description = "Snowbridge Router Primitives"
|
||||
version = "0.1.1"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.188", optional = true, features = ["derive"] }
|
||||
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false }
|
||||
scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
|
||||
log = { version = "0.4.20", default-features = false }
|
||||
|
||||
frame-support = { path = "../../../../../substrate/frame/support", default-features = false }
|
||||
frame-system = { path = "../../../../../substrate/frame/system", default-features = false }
|
||||
sp-core = { path = "../../../../../substrate/primitives/core", default-features = false }
|
||||
sp-io = { path = "../../../../../substrate/primitives/io", default-features = false }
|
||||
sp-runtime = { path = "../../../../../substrate/primitives/runtime", default-features = false }
|
||||
sp-std = { path = "../../../../../substrate/primitives/std", default-features = false }
|
||||
|
||||
xcm = { package = "staging-xcm", path = "../../../../../polkadot/xcm", default-features = false }
|
||||
xcm-builder = { package = "staging-xcm-builder", path = "../../../../../polkadot/xcm/xcm-builder", default-features = false }
|
||||
xcm-executor = { package = "staging-xcm-executor", path = "../../../../../polkadot/xcm/xcm-executor", default-features = false }
|
||||
|
||||
snowbridge-core = { path = "../../primitives/core", default-features = false }
|
||||
|
||||
ethabi = { git = "https://github.com/Snowfork/ethabi-decode.git", package = "ethabi-decode", branch = "master", default-features = false }
|
||||
|
||||
hex-literal = { version = "0.4.1" }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = { package = "rustc-hex", version = "2.1.0" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"ethabi/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"log/std",
|
||||
"scale-info/std",
|
||||
"serde",
|
||||
"snowbridge-core/std",
|
||||
"sp-core/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
"xcm-builder/std",
|
||||
"xcm-executor/std",
|
||||
"xcm/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"frame-support/runtime-benchmarks",
|
||||
"frame-system/runtime-benchmarks",
|
||||
"snowbridge-core/runtime-benchmarks",
|
||||
"sp-runtime/runtime-benchmarks",
|
||||
"xcm-builder/runtime-benchmarks",
|
||||
"xcm-executor/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,320 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Converts messages from Ethereum to XCM messages
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use core::marker::PhantomData;
|
||||
use frame_support::{traits::tokens::Balance as BalanceT, weights::Weight, PalletError};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::{Get, RuntimeDebug, H160};
|
||||
use sp_io::hashing::blake2_256;
|
||||
use sp_runtime::MultiAddress;
|
||||
use sp_std::prelude::*;
|
||||
use xcm::prelude::{Junction::AccountKey20, *};
|
||||
use xcm_executor::traits::ConvertLocation;
|
||||
|
||||
const MINIMUM_DEPOSIT: u128 = 1;
|
||||
|
||||
/// Messages from Ethereum are versioned. This is because in future,
|
||||
/// we may want to evolve the protocol so that the ethereum side sends XCM messages directly.
|
||||
/// Instead having BridgeHub transcode the messages into XCM.
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug)]
|
||||
pub enum VersionedMessage {
|
||||
V1(MessageV1),
|
||||
}
|
||||
|
||||
/// For V1, the ethereum side sends messages which are transcoded into XCM. These messages are
|
||||
/// self-contained, in that they can be transcoded using only information in the message.
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug)]
|
||||
pub struct MessageV1 {
|
||||
/// EIP-155 chain id of the origin Ethereum network
|
||||
pub chain_id: u64,
|
||||
/// The command originating from the Gateway contract
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug)]
|
||||
pub enum Command {
|
||||
/// Register a wrapped token on the AssetHub `ForeignAssets` pallet
|
||||
RegisterToken {
|
||||
/// The address of the ERC20 token to be bridged over to AssetHub
|
||||
token: H160,
|
||||
/// XCM execution fee on AssetHub
|
||||
fee: u128,
|
||||
},
|
||||
/// Send a token to AssetHub or another parachain
|
||||
SendToken {
|
||||
/// The address of the ERC20 token to be bridged over to AssetHub
|
||||
token: H160,
|
||||
/// The destination for the transfer
|
||||
destination: Destination,
|
||||
/// Amount to transfer
|
||||
amount: u128,
|
||||
/// XCM execution fee on AssetHub
|
||||
fee: u128,
|
||||
},
|
||||
}
|
||||
|
||||
/// Destination for bridged tokens
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug)]
|
||||
pub enum Destination {
|
||||
/// The funds will be deposited into account `id` on AssetHub
|
||||
AccountId32 { id: [u8; 32] },
|
||||
/// The funds will deposited into the sovereign account of destination parachain `para_id` on
|
||||
/// AssetHub, Account `id` on the destination parachain will receive the funds via a
|
||||
/// reserve-backed transfer. See <https://github.com/paritytech/xcm-format#depositreserveasset>
|
||||
ForeignAccountId32 {
|
||||
para_id: u32,
|
||||
id: [u8; 32],
|
||||
/// XCM execution fee on final destination
|
||||
fee: u128,
|
||||
},
|
||||
/// The funds will deposited into the sovereign account of destination parachain `para_id` on
|
||||
/// AssetHub, Account `id` on the destination parachain will receive the funds via a
|
||||
/// reserve-backed transfer. See <https://github.com/paritytech/xcm-format#depositreserveasset>
|
||||
ForeignAccountId20 {
|
||||
para_id: u32,
|
||||
id: [u8; 20],
|
||||
/// XCM execution fee on final destination
|
||||
fee: u128,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct MessageToXcm<
|
||||
CreateAssetCall,
|
||||
CreateAssetDeposit,
|
||||
InboundQueuePalletInstance,
|
||||
AccountId,
|
||||
Balance,
|
||||
> where
|
||||
CreateAssetCall: Get<CallIndex>,
|
||||
CreateAssetDeposit: Get<u128>,
|
||||
Balance: BalanceT,
|
||||
{
|
||||
_phantom: PhantomData<(
|
||||
CreateAssetCall,
|
||||
CreateAssetDeposit,
|
||||
InboundQueuePalletInstance,
|
||||
AccountId,
|
||||
Balance,
|
||||
)>,
|
||||
}
|
||||
|
||||
/// Reason why a message conversion failed.
|
||||
#[derive(Copy, Clone, TypeInfo, PalletError, Encode, Decode, RuntimeDebug)]
|
||||
pub enum ConvertMessageError {
|
||||
/// The message version is not supported for conversion.
|
||||
UnsupportedVersion,
|
||||
}
|
||||
|
||||
/// convert the inbound message to xcm which will be forwarded to the destination chain
|
||||
pub trait ConvertMessage {
|
||||
type Balance: BalanceT + From<u128>;
|
||||
type AccountId;
|
||||
/// Converts a versioned message into an XCM message and an optional topicID
|
||||
fn convert(message: VersionedMessage) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>;
|
||||
}
|
||||
|
||||
pub type CallIndex = [u8; 2];
|
||||
|
||||
impl<CreateAssetCall, CreateAssetDeposit, InboundQueuePalletInstance, AccountId, Balance>
|
||||
ConvertMessage
|
||||
for MessageToXcm<
|
||||
CreateAssetCall,
|
||||
CreateAssetDeposit,
|
||||
InboundQueuePalletInstance,
|
||||
AccountId,
|
||||
Balance,
|
||||
> where
|
||||
CreateAssetCall: Get<CallIndex>,
|
||||
CreateAssetDeposit: Get<u128>,
|
||||
InboundQueuePalletInstance: Get<u8>,
|
||||
Balance: BalanceT + From<u128>,
|
||||
AccountId: Into<[u8; 32]>,
|
||||
{
|
||||
type Balance = Balance;
|
||||
type AccountId = AccountId;
|
||||
|
||||
fn convert(message: VersionedMessage) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError> {
|
||||
use Command::*;
|
||||
use VersionedMessage::*;
|
||||
match message {
|
||||
V1(MessageV1 { chain_id, command: RegisterToken { token, fee } }) =>
|
||||
Ok(Self::convert_register_token(chain_id, token, fee)),
|
||||
V1(MessageV1 { chain_id, command: SendToken { token, destination, amount, fee } }) =>
|
||||
Ok(Self::convert_send_token(chain_id, token, destination, amount, fee)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<CreateAssetCall, CreateAssetDeposit, InboundQueuePalletInstance, AccountId, Balance>
|
||||
MessageToXcm<CreateAssetCall, CreateAssetDeposit, InboundQueuePalletInstance, AccountId, Balance>
|
||||
where
|
||||
CreateAssetCall: Get<CallIndex>,
|
||||
CreateAssetDeposit: Get<u128>,
|
||||
InboundQueuePalletInstance: Get<u8>,
|
||||
Balance: BalanceT + From<u128>,
|
||||
AccountId: Into<[u8; 32]>,
|
||||
{
|
||||
fn convert_register_token(chain_id: u64, token: H160, fee: u128) -> (Xcm<()>, Balance) {
|
||||
let network = Ethereum { chain_id };
|
||||
let xcm_fee: MultiAsset = (MultiLocation::parent(), fee).into();
|
||||
let deposit: MultiAsset = (MultiLocation::parent(), CreateAssetDeposit::get()).into();
|
||||
|
||||
let total_amount = fee + CreateAssetDeposit::get();
|
||||
let total: MultiAsset = (MultiLocation::parent(), total_amount).into();
|
||||
|
||||
let bridge_location: MultiLocation = (Parent, Parent, GlobalConsensus(network)).into();
|
||||
|
||||
let owner = GlobalConsensusEthereumConvertsFor::<[u8; 32]>::from_chain_id(&chain_id);
|
||||
let asset_id = Self::convert_token_address(network, token);
|
||||
let create_call_index: [u8; 2] = CreateAssetCall::get();
|
||||
let inbound_queue_pallet_index = InboundQueuePalletInstance::get();
|
||||
|
||||
let xcm: Xcm<()> = vec![
|
||||
// Teleport required fees.
|
||||
ReceiveTeleportedAsset(total.into()),
|
||||
// Pay for execution.
|
||||
BuyExecution { fees: xcm_fee, weight_limit: Unlimited },
|
||||
// Fund the snowbridge sovereign with the required deposit for creation.
|
||||
DepositAsset { assets: Definite(deposit.into()), beneficiary: bridge_location },
|
||||
// Only our inbound-queue pallet is allowed to invoke `UniversalOrigin`
|
||||
DescendOrigin(X1(PalletInstance(inbound_queue_pallet_index))),
|
||||
// Change origin to the bridge.
|
||||
UniversalOrigin(GlobalConsensus(network)),
|
||||
// Call create_asset on foreign assets pallet.
|
||||
Transact {
|
||||
origin_kind: OriginKind::Xcm,
|
||||
require_weight_at_most: Weight::from_parts(400_000_000, 8_000),
|
||||
call: (
|
||||
create_call_index,
|
||||
asset_id,
|
||||
MultiAddress::<[u8; 32], ()>::Id(owner),
|
||||
MINIMUM_DEPOSIT,
|
||||
)
|
||||
.encode()
|
||||
.into(),
|
||||
},
|
||||
RefundSurplus,
|
||||
// Clear the origin so that remaining assets in holding
|
||||
// are claimable by the physical origin (BridgeHub)
|
||||
ClearOrigin,
|
||||
]
|
||||
.into();
|
||||
|
||||
(xcm, total_amount.into())
|
||||
}
|
||||
|
||||
fn convert_send_token(
|
||||
chain_id: u64,
|
||||
token: H160,
|
||||
destination: Destination,
|
||||
amount: u128,
|
||||
asset_hub_fee: u128,
|
||||
) -> (Xcm<()>, Balance) {
|
||||
let network = Ethereum { chain_id };
|
||||
let asset_hub_fee_asset: MultiAsset = (MultiLocation::parent(), asset_hub_fee).into();
|
||||
let asset: MultiAsset = (Self::convert_token_address(network, token), amount).into();
|
||||
|
||||
let (dest_para_id, beneficiary, dest_para_fee) = match destination {
|
||||
// Final destination is a 32-byte account on AssetHub
|
||||
Destination::AccountId32 { id } => (
|
||||
None,
|
||||
MultiLocation { parents: 0, interior: X1(AccountId32 { network: None, id }) },
|
||||
0,
|
||||
),
|
||||
// Final destination is a 32-byte account on a sibling of AssetHub
|
||||
Destination::ForeignAccountId32 { para_id, id, fee } => (
|
||||
Some(para_id),
|
||||
MultiLocation { parents: 0, interior: X1(AccountId32 { network: None, id }) },
|
||||
// Total fee needs to cover execution on AssetHub and Sibling
|
||||
fee,
|
||||
),
|
||||
// Final destination is a 20-byte account on a sibling of AssetHub
|
||||
Destination::ForeignAccountId20 { para_id, id, fee } => (
|
||||
Some(para_id),
|
||||
MultiLocation { parents: 0, interior: X1(AccountKey20 { network: None, key: id }) },
|
||||
// Total fee needs to cover execution on AssetHub and Sibling
|
||||
fee,
|
||||
),
|
||||
};
|
||||
|
||||
let total_fees = asset_hub_fee.saturating_add(dest_para_fee);
|
||||
let total_fee_asset: MultiAsset = (MultiLocation::parent(), total_fees).into();
|
||||
let inbound_queue_pallet_index = InboundQueuePalletInstance::get();
|
||||
|
||||
let mut instructions = vec![
|
||||
ReceiveTeleportedAsset(total_fee_asset.into()),
|
||||
BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited },
|
||||
DescendOrigin(X1(PalletInstance(inbound_queue_pallet_index))),
|
||||
UniversalOrigin(GlobalConsensus(network)),
|
||||
ReserveAssetDeposited(asset.clone().into()),
|
||||
ClearOrigin,
|
||||
];
|
||||
|
||||
match dest_para_id {
|
||||
Some(dest_para_id) => {
|
||||
let dest_para_fee_asset: MultiAsset =
|
||||
(MultiLocation::parent(), dest_para_fee).into();
|
||||
|
||||
instructions.extend(vec![
|
||||
// Perform a deposit reserve to send to destination chain.
|
||||
DepositReserveAsset {
|
||||
assets: Definite(vec![dest_para_fee_asset.clone(), asset.clone()].into()),
|
||||
dest: MultiLocation { parents: 1, interior: X1(Parachain(dest_para_id)) },
|
||||
xcm: vec![
|
||||
// Buy execution on target.
|
||||
BuyExecution { fees: dest_para_fee_asset, weight_limit: Unlimited },
|
||||
// Deposit asset to beneficiary.
|
||||
DepositAsset { assets: Definite(asset.into()), beneficiary },
|
||||
]
|
||||
.into(),
|
||||
},
|
||||
]);
|
||||
},
|
||||
None => {
|
||||
instructions.extend(vec![
|
||||
// Deposit asset to beneficiary.
|
||||
DepositAsset { assets: Definite(asset.into()), beneficiary },
|
||||
]);
|
||||
},
|
||||
}
|
||||
|
||||
(instructions.into(), total_fees.into())
|
||||
}
|
||||
|
||||
// Convert ERC20 token address to a Multilocation that can be understood by Assets Hub.
|
||||
fn convert_token_address(network: NetworkId, token: H160) -> MultiLocation {
|
||||
MultiLocation {
|
||||
parents: 2,
|
||||
interior: X2(
|
||||
GlobalConsensus(network),
|
||||
AccountKey20 { network: None, key: token.into() },
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GlobalConsensusEthereumConvertsFor<AccountId>(PhantomData<AccountId>);
|
||||
impl<AccountId> ConvertLocation<AccountId> for GlobalConsensusEthereumConvertsFor<AccountId>
|
||||
where
|
||||
AccountId: From<[u8; 32]> + Clone,
|
||||
{
|
||||
fn convert_location(location: &MultiLocation) -> Option<AccountId> {
|
||||
if let MultiLocation { interior: X1(GlobalConsensus(Ethereum { chain_id })), .. } = location
|
||||
{
|
||||
Some(Self::from_chain_id(chain_id).into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<AccountId> GlobalConsensusEthereumConvertsFor<AccountId> {
|
||||
pub fn from_chain_id(chain_id: &u64) -> [u8; 32] {
|
||||
(b"ethereum-chain", chain_id).using_encoded(blake2_256)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use super::GlobalConsensusEthereumConvertsFor;
|
||||
use crate::inbound::CallIndex;
|
||||
use frame_support::parameter_types;
|
||||
use hex_literal::hex;
|
||||
use xcm::v3::prelude::*;
|
||||
use xcm_executor::traits::ConvertLocation;
|
||||
|
||||
const NETWORK: NetworkId = Ethereum { chain_id: 11155111 };
|
||||
|
||||
parameter_types! {
|
||||
pub EthereumNetwork: NetworkId = NETWORK;
|
||||
|
||||
pub const CreateAssetCall: CallIndex = [1, 1];
|
||||
pub const CreateAssetExecutionFee: u128 = 123;
|
||||
pub const CreateAssetDeposit: u128 = 891;
|
||||
pub const SendTokenExecutionFee: u128 = 592;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contract_location_with_network_converts_successfully() {
|
||||
let expected_account: [u8; 32] =
|
||||
hex!("ce796ae65569a670d0c1cc1ac12515a3ce21b5fbf729d63d7b289baad070139d");
|
||||
let contract_location = MultiLocation { parents: 2, interior: X1(GlobalConsensus(NETWORK)) };
|
||||
|
||||
let account =
|
||||
GlobalConsensusEthereumConvertsFor::<[u8; 32]>::convert_location(&contract_location)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(account, expected_account);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contract_location_with_incorrect_location_fails_convert() {
|
||||
let contract_location =
|
||||
MultiLocation { parents: 2, interior: X2(GlobalConsensus(Polkadot), Parachain(1000)) };
|
||||
|
||||
assert_eq!(
|
||||
GlobalConsensusEthereumConvertsFor::<[u8; 32]>::convert_location(&contract_location),
|
||||
None,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
pub mod inbound;
|
||||
pub mod outbound;
|
||||
@@ -0,0 +1,282 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Converts XCM messages into simpler commands that can be processed by the Gateway contract
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use core::slice::Iter;
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
|
||||
use frame_support::{ensure, traits::Get};
|
||||
use snowbridge_core::{
|
||||
outbound::{AgentExecuteCommand, Command, Message, SendMessage},
|
||||
ChannelId, ParaId,
|
||||
};
|
||||
use sp_core::{H160, H256};
|
||||
use sp_std::{iter::Peekable, marker::PhantomData, prelude::*};
|
||||
use xcm::v3::prelude::*;
|
||||
use xcm_executor::traits::{ConvertLocation, ExportXcm};
|
||||
|
||||
pub struct EthereumBlobExporter<
|
||||
UniversalLocation,
|
||||
EthereumNetwork,
|
||||
OutboundQueue,
|
||||
AgentHashedDescription,
|
||||
>(PhantomData<(UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription)>);
|
||||
|
||||
impl<UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription> ExportXcm
|
||||
for EthereumBlobExporter<UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription>
|
||||
where
|
||||
UniversalLocation: Get<InteriorMultiLocation>,
|
||||
EthereumNetwork: Get<NetworkId>,
|
||||
OutboundQueue: SendMessage<Balance = u128>,
|
||||
AgentHashedDescription: ConvertLocation<H256>,
|
||||
{
|
||||
type Ticket = (Vec<u8>, XcmHash);
|
||||
|
||||
fn validate(
|
||||
network: NetworkId,
|
||||
_channel: u32,
|
||||
universal_source: &mut Option<InteriorMultiLocation>,
|
||||
destination: &mut Option<InteriorMultiLocation>,
|
||||
message: &mut Option<Xcm<()>>,
|
||||
) -> SendResult<Self::Ticket> {
|
||||
let expected_network = EthereumNetwork::get();
|
||||
let universal_location = UniversalLocation::get();
|
||||
|
||||
if network != expected_network {
|
||||
log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched bridge network {network:?}.");
|
||||
return Err(SendError::NotApplicable)
|
||||
}
|
||||
|
||||
let dest = destination.take().ok_or(SendError::MissingArgument)?;
|
||||
if dest != Here {
|
||||
log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched remote destination {dest:?}.");
|
||||
return Err(SendError::NotApplicable)
|
||||
}
|
||||
|
||||
let (local_net, local_sub) = universal_source
|
||||
.take()
|
||||
.ok_or_else(|| {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "universal source not provided.");
|
||||
SendError::MissingArgument
|
||||
})?
|
||||
.split_global()
|
||||
.map_err(|()| {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "could not get global consensus from universal source '{universal_source:?}'.");
|
||||
SendError::Unroutable
|
||||
})?;
|
||||
|
||||
if Ok(local_net) != universal_location.global_consensus() {
|
||||
log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched relay network {local_net:?}.");
|
||||
return Err(SendError::NotApplicable)
|
||||
}
|
||||
|
||||
let para_id = match local_sub {
|
||||
X1(Parachain(para_id)) => para_id,
|
||||
_ => {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "could not get parachain id from universal source '{local_sub:?}'.");
|
||||
return Err(SendError::MissingArgument)
|
||||
},
|
||||
};
|
||||
|
||||
let message = message.take().ok_or_else(|| {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "xcm message not provided.");
|
||||
SendError::MissingArgument
|
||||
})?;
|
||||
|
||||
let mut converter = XcmConverter::new(&message, &expected_network);
|
||||
let (agent_execute_command, message_id) = converter.convert().map_err(|err|{
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "unroutable due to pattern matching error '{err:?}'.");
|
||||
SendError::Unroutable
|
||||
})?;
|
||||
|
||||
let source_location: MultiLocation = MultiLocation { parents: 1, interior: local_sub };
|
||||
let agent_id = match AgentHashedDescription::convert_location(&source_location) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "unroutable due to not being able to create agent id. '{source_location:?}'");
|
||||
return Err(SendError::Unroutable)
|
||||
},
|
||||
};
|
||||
|
||||
let channel_id: ChannelId = ParaId::from(para_id).into();
|
||||
|
||||
let outbound_message = Message {
|
||||
id: Some(message_id.into()),
|
||||
channel_id,
|
||||
command: Command::AgentExecute { agent_id, command: agent_execute_command },
|
||||
};
|
||||
|
||||
// validate the message
|
||||
let (ticket, fee) = OutboundQueue::validate(&outbound_message).map_err(|err| {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "OutboundQueue validation of message failed. {err:?}");
|
||||
SendError::Unroutable
|
||||
})?;
|
||||
|
||||
// convert fee to MultiAsset
|
||||
let fee = MultiAsset::from((MultiLocation::parent(), fee.total())).into();
|
||||
|
||||
Ok(((ticket.encode(), message_id), fee))
|
||||
}
|
||||
|
||||
fn deliver(blob: (Vec<u8>, XcmHash)) -> Result<XcmHash, SendError> {
|
||||
let ticket: OutboundQueue::Ticket = OutboundQueue::Ticket::decode(&mut blob.0.as_ref())
|
||||
.map_err(|_| {
|
||||
log::trace!(target: "xcm::ethereum_blob_exporter", "undeliverable due to decoding error");
|
||||
SendError::NotApplicable
|
||||
})?;
|
||||
|
||||
let message_id = OutboundQueue::deliver(ticket).map_err(|_| {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "OutboundQueue submit of message failed");
|
||||
SendError::Transport("other transport error")
|
||||
})?;
|
||||
|
||||
log::info!(target: "xcm::ethereum_blob_exporter", "message delivered {message_id:#?}.");
|
||||
Ok(message_id.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can be thrown to the pattern matching step.
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum XcmConverterError {
|
||||
UnexpectedEndOfXcm,
|
||||
EndOfXcmMessageExpected,
|
||||
WithdrawAssetExpected,
|
||||
DepositAssetExpected,
|
||||
NoReserveAssets,
|
||||
FilterDoesNotConsumeAllAssets,
|
||||
TooManyAssets,
|
||||
ZeroAssetTransfer,
|
||||
BeneficiaryResolutionFailed,
|
||||
AssetResolutionFailed,
|
||||
InvalidFeeAsset,
|
||||
SetTopicExpected,
|
||||
}
|
||||
|
||||
macro_rules! match_expression {
|
||||
($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )?, $value:expr $(,)?) => {
|
||||
match $expression {
|
||||
$( $pattern )|+ $( if $guard )? => Some($value),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
struct XcmConverter<'a, Call> {
|
||||
iter: Peekable<Iter<'a, Instruction<Call>>>,
|
||||
ethereum_network: &'a NetworkId,
|
||||
}
|
||||
impl<'a, Call> XcmConverter<'a, Call> {
|
||||
fn new(message: &'a Xcm<Call>, ethereum_network: &'a NetworkId) -> Self {
|
||||
Self { iter: message.inner().iter().peekable(), ethereum_network }
|
||||
}
|
||||
|
||||
fn convert(&mut self) -> Result<(AgentExecuteCommand, [u8; 32]), XcmConverterError> {
|
||||
// Get withdraw/deposit and make native tokens create message.
|
||||
let result = self.native_tokens_unlock_message()?;
|
||||
|
||||
// All xcm instructions must be consumed before exit.
|
||||
if self.next().is_ok() {
|
||||
return Err(XcmConverterError::EndOfXcmMessageExpected)
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn native_tokens_unlock_message(
|
||||
&mut self,
|
||||
) -> Result<(AgentExecuteCommand, [u8; 32]), XcmConverterError> {
|
||||
use XcmConverterError::*;
|
||||
|
||||
// Get the reserve assets from WithdrawAsset.
|
||||
let reserve_assets =
|
||||
match_expression!(self.next()?, WithdrawAsset(reserve_assets), reserve_assets)
|
||||
.ok_or(WithdrawAssetExpected)?;
|
||||
|
||||
// Check if clear origin exists and skip over it.
|
||||
if match_expression!(self.peek(), Ok(ClearOrigin), ()).is_some() {
|
||||
let _ = self.next();
|
||||
}
|
||||
|
||||
// Get the fee asset item from BuyExecution or continue parsing.
|
||||
let fee_asset = match_expression!(self.peek(), Ok(BuyExecution { fees, .. }), fees);
|
||||
if fee_asset.is_some() {
|
||||
let _ = self.next();
|
||||
}
|
||||
|
||||
let (deposit_assets, beneficiary) = match_expression!(
|
||||
self.next()?,
|
||||
DepositAsset { assets, beneficiary },
|
||||
(assets, beneficiary)
|
||||
)
|
||||
.ok_or(DepositAssetExpected)?;
|
||||
|
||||
// assert that the beneficiary is AccountKey20.
|
||||
let recipient = match_expression!(
|
||||
beneficiary,
|
||||
MultiLocation { parents: 0, interior: X1(AccountKey20 { network, key }) }
|
||||
if self.network_matches(network),
|
||||
H160(*key)
|
||||
)
|
||||
.ok_or(BeneficiaryResolutionFailed)?;
|
||||
|
||||
// Make sure there are reserved assets.
|
||||
if reserve_assets.len() == 0 {
|
||||
return Err(NoReserveAssets)
|
||||
}
|
||||
|
||||
// Check the the deposit asset filter matches what was reserved.
|
||||
if reserve_assets.inner().iter().any(|asset| !deposit_assets.matches(asset)) {
|
||||
return Err(FilterDoesNotConsumeAllAssets)
|
||||
}
|
||||
|
||||
// We only support a single asset at a time.
|
||||
ensure!(reserve_assets.len() == 1, TooManyAssets);
|
||||
let reserve_asset = reserve_assets.get(0).ok_or(AssetResolutionFailed)?;
|
||||
|
||||
// If there was a fee specified verify it.
|
||||
if let Some(fee_asset) = fee_asset {
|
||||
// The fee asset must be the same as the reserve asset.
|
||||
if fee_asset.id != reserve_asset.id || fee_asset.fun > reserve_asset.fun {
|
||||
return Err(InvalidFeeAsset)
|
||||
}
|
||||
}
|
||||
|
||||
let (token, amount) = match_expression!(
|
||||
reserve_asset,
|
||||
MultiAsset {
|
||||
id: Concrete(MultiLocation { parents: 0, interior: X1(AccountKey20 { network , key })}),
|
||||
fun: Fungible(amount)
|
||||
} if self.network_matches(network),
|
||||
(H160(*key), *amount)
|
||||
)
|
||||
.ok_or(AssetResolutionFailed)?;
|
||||
|
||||
// transfer amount must be greater than 0.
|
||||
ensure!(amount > 0, ZeroAssetTransfer);
|
||||
|
||||
// Check if there is a SetTopic and skip over it if found.
|
||||
let topic_id = match_expression!(self.next()?, SetTopic(id), id).ok_or(SetTopicExpected)?;
|
||||
|
||||
Ok((AgentExecuteCommand::TransferToken { token, recipient, amount }, *topic_id))
|
||||
}
|
||||
|
||||
fn next(&mut self) -> Result<&'a Instruction<Call>, XcmConverterError> {
|
||||
self.iter.next().ok_or(XcmConverterError::UnexpectedEndOfXcm)
|
||||
}
|
||||
|
||||
fn peek(&mut self) -> Result<&&'a Instruction<Call>, XcmConverterError> {
|
||||
self.iter.peek().ok_or(XcmConverterError::UnexpectedEndOfXcm)
|
||||
}
|
||||
|
||||
fn network_matches(&self, network: &Option<NetworkId>) -> bool {
|
||||
if let Some(network) = network {
|
||||
network == self.ethereum_network
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "snowbridge-rococo-common"
|
||||
description = "Snowbridge Rococo Common"
|
||||
version = "0.0.1"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
log = { version = "0.4.20", default-features = false }
|
||||
|
||||
frame-support = { path = "../../../../../substrate/frame/support", default-features = false }
|
||||
xcm = { package = "staging-xcm", path = "../../../../../polkadot/xcm", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"frame-support/std",
|
||||
"log/std",
|
||||
"xcm/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"frame-support/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! # Rococo Common
|
||||
//!
|
||||
//! Config used for the Rococo asset hub and bridge hub runtimes.
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use frame_support::parameter_types;
|
||||
use xcm::opaque::lts::NetworkId;
|
||||
|
||||
pub const INBOUND_QUEUE_MESSAGES_PALLET_INDEX: u8 = 80;
|
||||
|
||||
parameter_types! {
|
||||
// Network and location for the Ethereum chain.
|
||||
pub EthereumNetwork: NetworkId = NetworkId::Ethereum { chain_id: 11155111 };
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
[package]
|
||||
name = "snowbridge-runtime-common"
|
||||
description = "Snowbridge Runtime Common"
|
||||
version = "0.1.1"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
log = { version = "0.4.20", default-features = false }
|
||||
|
||||
frame-support = { path = "../../../../../substrate/frame/support", default-features = false }
|
||||
frame-system = { path = "../../../../../substrate/frame/system", default-features = false }
|
||||
sp-arithmetic = { path = "../../../../../substrate/primitives/arithmetic", default-features = false }
|
||||
xcm = { package = "staging-xcm", path = "../../../../../polkadot/xcm", default-features = false }
|
||||
xcm-builder = { package = "staging-xcm-builder", path = "../../../../../polkadot/xcm/xcm-builder", default-features = false }
|
||||
xcm-executor = { package = "staging-xcm-executor", path = "../../../../../polkadot/xcm/xcm-executor", default-features = false }
|
||||
|
||||
snowbridge-core = { path = "../../primitives/core", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"log/std",
|
||||
"snowbridge-core/std",
|
||||
"sp-arithmetic/std",
|
||||
"xcm-builder/std",
|
||||
"xcm-executor/std",
|
||||
"xcm/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"frame-support/runtime-benchmarks",
|
||||
"frame-system/runtime-benchmarks",
|
||||
"snowbridge-core/runtime-benchmarks",
|
||||
"xcm-builder/runtime-benchmarks",
|
||||
"xcm-executor/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,129 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! # Runtime Common
|
||||
//!
|
||||
//! Common traits and types shared by runtimes.
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use core::marker::PhantomData;
|
||||
use frame_support::traits::Get;
|
||||
use snowbridge_core::{outbound::SendMessageFeeProvider, sibling_sovereign_account_raw};
|
||||
use sp_arithmetic::traits::{BaseArithmetic, Unsigned};
|
||||
use xcm::prelude::*;
|
||||
use xcm_builder::{deposit_or_burn_fee, HandleFee};
|
||||
use xcm_executor::traits::{FeeReason, TransactAsset};
|
||||
|
||||
/// A `HandleFee` implementation that takes fees from `ExportMessage` XCM instructions
|
||||
/// to Snowbridge and splits off the remote fee and deposits it to the origin
|
||||
/// parachain sovereign account. The local fee is then returned back to be handled by
|
||||
/// the next fee handler in the chain. Most likely the treasury account.
|
||||
pub struct XcmExportFeeToSibling<
|
||||
Balance,
|
||||
AccountId,
|
||||
FeeAssetLocation,
|
||||
EthereumNetwork,
|
||||
AssetTransactor,
|
||||
FeeProvider,
|
||||
>(
|
||||
PhantomData<(
|
||||
Balance,
|
||||
AccountId,
|
||||
FeeAssetLocation,
|
||||
EthereumNetwork,
|
||||
AssetTransactor,
|
||||
FeeProvider,
|
||||
)>,
|
||||
);
|
||||
|
||||
impl<Balance, AccountId, FeeAssetLocation, EthereumNetwork, AssetTransactor, FeeProvider> HandleFee
|
||||
for XcmExportFeeToSibling<
|
||||
Balance,
|
||||
AccountId,
|
||||
FeeAssetLocation,
|
||||
EthereumNetwork,
|
||||
AssetTransactor,
|
||||
FeeProvider,
|
||||
> where
|
||||
Balance: BaseArithmetic + Unsigned + Copy + From<u128> + Into<u128>,
|
||||
AccountId: Clone + Into<[u8; 32]> + From<[u8; 32]>,
|
||||
FeeAssetLocation: Get<MultiLocation>,
|
||||
EthereumNetwork: Get<NetworkId>,
|
||||
AssetTransactor: TransactAsset,
|
||||
FeeProvider: SendMessageFeeProvider<Balance = Balance>,
|
||||
{
|
||||
fn handle_fee(
|
||||
fees: MultiAssets,
|
||||
context: Option<&XcmContext>,
|
||||
reason: FeeReason,
|
||||
) -> MultiAssets {
|
||||
let token_location = FeeAssetLocation::get();
|
||||
|
||||
// Check the reason to see if this export is for snowbridge.
|
||||
if !matches!(
|
||||
reason,
|
||||
FeeReason::Export { network: bridged_network, destination }
|
||||
if bridged_network == EthereumNetwork::get() && destination == Here
|
||||
) {
|
||||
return fees
|
||||
}
|
||||
|
||||
// Get the parachain sovereign from the `context`.
|
||||
let para_sovereign = if let Some(XcmContext {
|
||||
origin: Some(MultiLocation { parents: 1, interior }),
|
||||
..
|
||||
}) = context
|
||||
{
|
||||
if let Some(Parachain(sibling_para_id)) = interior.first() {
|
||||
let account: AccountId =
|
||||
sibling_sovereign_account_raw((*sibling_para_id).into()).into();
|
||||
account
|
||||
} else {
|
||||
return fees
|
||||
}
|
||||
} else {
|
||||
return fees
|
||||
};
|
||||
|
||||
// Get the total fee offered by export message.
|
||||
let maybe_total_supplied_fee: Option<(usize, Balance)> = fees
|
||||
.inner()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, asset)| {
|
||||
if let MultiAsset { id: Concrete(location), fun: Fungible(amount) } = asset {
|
||||
if *location == token_location {
|
||||
return Some((index, (*amount).into()))
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.next();
|
||||
|
||||
if let Some((fee_index, total_fee)) = maybe_total_supplied_fee {
|
||||
let remote_fee = total_fee.saturating_sub(FeeProvider::local_fee());
|
||||
if remote_fee > (0u128).into() {
|
||||
// Refund remote component of fee to physical origin
|
||||
deposit_or_burn_fee::<AssetTransactor, _>(
|
||||
MultiAsset { id: Concrete(token_location), fun: Fungible(remote_fee.into()) }
|
||||
.into(),
|
||||
context,
|
||||
para_sovereign,
|
||||
);
|
||||
// Return remaining fee to the next fee handler in the chain.
|
||||
let mut modified_fees = fees.inner().clone();
|
||||
modified_fees.remove(fee_index);
|
||||
modified_fees.push(MultiAsset {
|
||||
id: Concrete(token_location),
|
||||
fun: Fungible((total_fee - remote_fee).into()),
|
||||
});
|
||||
return modified_fees.into()
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
target: "xcm::fees",
|
||||
"XcmExportFeeToSibling skipped: {fees:?}, context: {context:?}, reason: {reason:?}",
|
||||
);
|
||||
fees
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
[package]
|
||||
name = "snowbridge-runtime-tests"
|
||||
description = "Snowbridge Runtime Tests"
|
||||
version = "0.1.0"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] }
|
||||
hex-literal = { version = "0.4.1" }
|
||||
log = { version = "0.4.20", default-features = false }
|
||||
scale-info = { version = "2.10.0", default-features = false, features = ["derive"] }
|
||||
serde = { version = "1.0.188", optional = true, features = ["derive"] }
|
||||
smallvec = "1.11.0"
|
||||
|
||||
# Substrate
|
||||
frame-benchmarking = { path = "../../../../../substrate/frame/benchmarking", default-features = false, optional = true }
|
||||
frame-executive = { path = "../../../../../substrate/frame/executive", default-features = false }
|
||||
frame-support = { path = "../../../../../substrate/frame/support", default-features = false }
|
||||
frame-system = { path = "../../../../../substrate/frame/system", default-features = false }
|
||||
frame-system-benchmarking = { path = "../../../../../substrate/frame/system/benchmarking", default-features = false, optional = true }
|
||||
frame-system-rpc-runtime-api = { path = "../../../../../substrate/frame/system/rpc/runtime-api", default-features = false }
|
||||
frame-try-runtime = { path = "../../../../../substrate/frame/try-runtime", default-features = false, optional = true }
|
||||
pallet-aura = { path = "../../../../../substrate/frame/aura", default-features = false }
|
||||
pallet-authorship = { path = "../../../../../substrate/frame/authorship", default-features = false }
|
||||
pallet-balances = { path = "../../../../../substrate/frame/balances", default-features = false }
|
||||
pallet-session = { path = "../../../../../substrate/frame/session", default-features = false }
|
||||
pallet-multisig = { path = "../../../../../substrate/frame/multisig", default-features = false }
|
||||
pallet-message-queue = { path = "../../../../../substrate/frame/message-queue", default-features = false }
|
||||
pallet-timestamp = { path = "../../../../../substrate/frame/timestamp", default-features = false }
|
||||
pallet-transaction-payment = { path = "../../../../../substrate/frame/transaction-payment", default-features = false }
|
||||
pallet-transaction-payment-rpc-runtime-api = { path = "../../../../../substrate/frame/transaction-payment/rpc/runtime-api", default-features = false }
|
||||
pallet-utility = { path = "../../../../../substrate/frame/utility", default-features = false }
|
||||
sp-api = { path = "../../../../../substrate/primitives/api", default-features = false }
|
||||
sp-block-builder = { path = "../../../../../substrate/primitives/block-builder", default-features = false }
|
||||
sp-consensus-aura = { path = "../../../../../substrate/primitives/consensus/aura", default-features = false }
|
||||
sp-core = { path = "../../../../../substrate/primitives/core", default-features = false }
|
||||
sp-genesis-builder = { path = "../../../../../substrate/primitives/genesis-builder", default-features = false }
|
||||
sp-inherents = { path = "../../../../../substrate/primitives/inherents", default-features = false }
|
||||
sp-io = { path = "../../../../../substrate/primitives/io", default-features = false }
|
||||
sp-offchain = { path = "../../../../../substrate/primitives/offchain", default-features = false }
|
||||
sp-runtime = { path = "../../../../../substrate/primitives/runtime", default-features = false }
|
||||
sp-session = { path = "../../../../../substrate/primitives/session", default-features = false }
|
||||
sp-std = { path = "../../../../../substrate/primitives/std", default-features = false }
|
||||
sp-storage = { path = "../../../../../substrate/primitives/storage", default-features = false }
|
||||
sp-transaction-pool = { path = "../../../../../substrate/primitives/transaction-pool", default-features = false }
|
||||
sp-version = { path = "../../../../../substrate/primitives/version", default-features = false }
|
||||
|
||||
# Polkadot
|
||||
rococo-runtime-constants = { path = "../../../../../polkadot/runtime/rococo/constants", default-features = false }
|
||||
pallet-xcm = { path = "../../../../../polkadot/xcm/pallet-xcm", default-features = false }
|
||||
pallet-xcm-benchmarks = { path = "../../../../../polkadot/xcm/pallet-xcm-benchmarks", default-features = false, optional = true }
|
||||
polkadot-core-primitives = { path = "../../../../../polkadot/core-primitives", default-features = false }
|
||||
polkadot-parachain-primitives = { path = "../../../../../polkadot/parachain", default-features = false }
|
||||
polkadot-runtime-common = { path = "../../../../../polkadot/runtime/common", default-features = false }
|
||||
xcm = { package = "staging-xcm", path = "../../../../../polkadot/xcm", default-features = false }
|
||||
xcm-builder = { package = "staging-xcm-builder", path = "../../../../../polkadot/xcm/xcm-builder", default-features = false }
|
||||
xcm-executor = { package = "staging-xcm-executor", path = "../../../../../polkadot/xcm/xcm-executor", default-features = false }
|
||||
|
||||
# Cumulus
|
||||
cumulus-pallet-aura-ext = { path = "../../../../../cumulus/pallets/aura-ext", default-features = false }
|
||||
cumulus-pallet-dmp-queue = { path = "../../../../../cumulus/pallets/dmp-queue", default-features = false }
|
||||
cumulus-pallet-parachain-system = { path = "../../../../../cumulus/pallets/parachain-system", default-features = false, features = ["parameterized-consensus-hook"] }
|
||||
cumulus-pallet-session-benchmarking = { path = "../../../../../cumulus/pallets/session-benchmarking", default-features = false }
|
||||
cumulus-pallet-xcm = { path = "../../../../../cumulus/pallets/xcm", default-features = false }
|
||||
cumulus-pallet-xcmp-queue = { path = "../../../../../cumulus/pallets/xcmp-queue", default-features = false, features = ["bridging"] }
|
||||
cumulus-primitives-core = { path = "../../../../../cumulus/primitives/core", default-features = false }
|
||||
cumulus-primitives-utility = { path = "../../../../../cumulus/primitives/utility", default-features = false }
|
||||
pallet-collator-selection = { path = "../../../../../cumulus/pallets/collator-selection", default-features = false }
|
||||
parachain-info = { package = "staging-parachain-info", path = "../../../../../cumulus/parachains/pallets/parachain-info", default-features = false }
|
||||
parachains-common = { path = "../../../../../cumulus/parachains/common", default-features = false }
|
||||
parachains-runtimes-test-utils = { path = "../../../../../cumulus/parachains/runtimes/test-utils", default-features = false }
|
||||
bridge-hub-rococo-runtime = { path = "../../../../../cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo", default-features = false }
|
||||
asset-hub-rococo-runtime = { path = "../../../../../cumulus/parachains/runtimes/assets/asset-hub-rococo", default-features = false }
|
||||
assets-common = { path = "../../../../../cumulus/parachains/runtimes/assets/common", default-features = false }
|
||||
|
||||
# Ethereum Bridge (Snowbridge)
|
||||
snowbridge-core = { path = "../../primitives/core", default-features = false }
|
||||
snowbridge-beacon-primitives = { path = "../../primitives/beacon", default-features = false }
|
||||
snowbridge-router-primitives = { path = "../../primitives/router", default-features = false }
|
||||
snowbridge-ethereum-beacon-client = { path = "../../pallets/ethereum-beacon-client", default-features = false }
|
||||
snowbridge-inbound-queue = { path = "../../pallets/inbound-queue", default-features = false }
|
||||
snowbridge-outbound-queue = { path = "../../pallets/outbound-queue", default-features = false }
|
||||
snowbridge-outbound-queue-runtime-api = { path = "../../pallets/outbound-queue/runtime-api", default-features = false }
|
||||
snowbridge-system = { path = "../../pallets/system", default-features = false }
|
||||
snowbridge-system-runtime-api = { path = "../../pallets/system/runtime-api", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
static_assertions = "1.1"
|
||||
bridge-hub-test-utils = { path = "../../../../../cumulus/parachains/runtimes/bridge-hubs/test-utils" }
|
||||
bridge-runtime-common = { path = "../../../../../bridges/bin/runtime-common", features = ["integrity-test"] }
|
||||
sp-keyring = { path = "../../../../../substrate/primitives/keyring" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"asset-hub-rococo-runtime/std",
|
||||
"assets-common/std",
|
||||
"bridge-hub-rococo-runtime/std",
|
||||
"codec/std",
|
||||
"cumulus-pallet-aura-ext/std",
|
||||
"cumulus-pallet-dmp-queue/std",
|
||||
"cumulus-pallet-parachain-system/std",
|
||||
"cumulus-pallet-session-benchmarking/std",
|
||||
"cumulus-pallet-xcm/std",
|
||||
"cumulus-pallet-xcmp-queue/std",
|
||||
"cumulus-primitives-core/std",
|
||||
"cumulus-primitives-utility/std",
|
||||
"frame-benchmarking/std",
|
||||
"frame-executive/std",
|
||||
"frame-support/std",
|
||||
"frame-system-benchmarking?/std",
|
||||
"frame-system-rpc-runtime-api/std",
|
||||
"frame-system/std",
|
||||
"frame-try-runtime?/std",
|
||||
"log/std",
|
||||
"pallet-aura/std",
|
||||
"pallet-authorship/std",
|
||||
"pallet-balances/std",
|
||||
"pallet-collator-selection/std",
|
||||
"pallet-message-queue/std",
|
||||
"pallet-multisig/std",
|
||||
"pallet-session/std",
|
||||
"pallet-timestamp/std",
|
||||
"pallet-transaction-payment-rpc-runtime-api/std",
|
||||
"pallet-transaction-payment/std",
|
||||
"pallet-utility/std",
|
||||
"pallet-xcm-benchmarks?/std",
|
||||
"pallet-xcm/std",
|
||||
"parachain-info/std",
|
||||
"parachains-common/std",
|
||||
"parachains-runtimes-test-utils/std",
|
||||
"polkadot-core-primitives/std",
|
||||
"polkadot-parachain-primitives/std",
|
||||
"polkadot-runtime-common/std",
|
||||
"rococo-runtime-constants/std",
|
||||
"scale-info/std",
|
||||
"serde",
|
||||
"snowbridge-beacon-primitives/std",
|
||||
"snowbridge-core/std",
|
||||
"snowbridge-ethereum-beacon-client/std",
|
||||
"snowbridge-inbound-queue/std",
|
||||
"snowbridge-outbound-queue-runtime-api/std",
|
||||
"snowbridge-outbound-queue/std",
|
||||
"snowbridge-router-primitives/std",
|
||||
"snowbridge-system-runtime-api/std",
|
||||
"snowbridge-system/std",
|
||||
"sp-api/std",
|
||||
"sp-block-builder/std",
|
||||
"sp-consensus-aura/std",
|
||||
"sp-core/std",
|
||||
"sp-genesis-builder/std",
|
||||
"sp-inherents/std",
|
||||
"sp-io/std",
|
||||
"sp-offchain/std",
|
||||
"sp-runtime/std",
|
||||
"sp-session/std",
|
||||
"sp-std/std",
|
||||
"sp-storage/std",
|
||||
"sp-transaction-pool/std",
|
||||
"sp-version/std",
|
||||
"xcm-builder/std",
|
||||
"xcm-executor/std",
|
||||
"xcm/std",
|
||||
]
|
||||
|
||||
runtime-benchmarks = [
|
||||
"asset-hub-rococo-runtime/runtime-benchmarks",
|
||||
"assets-common/runtime-benchmarks",
|
||||
"bridge-hub-rococo-runtime/runtime-benchmarks",
|
||||
"bridge-runtime-common/runtime-benchmarks",
|
||||
"cumulus-pallet-dmp-queue/runtime-benchmarks",
|
||||
"cumulus-pallet-parachain-system/runtime-benchmarks",
|
||||
"cumulus-pallet-session-benchmarking/runtime-benchmarks",
|
||||
"cumulus-pallet-xcmp-queue/runtime-benchmarks",
|
||||
"cumulus-primitives-core/runtime-benchmarks",
|
||||
"cumulus-primitives-utility/runtime-benchmarks",
|
||||
"frame-benchmarking/runtime-benchmarks",
|
||||
"frame-support/runtime-benchmarks",
|
||||
"frame-system-benchmarking/runtime-benchmarks",
|
||||
"frame-system/runtime-benchmarks",
|
||||
"pallet-balances/runtime-benchmarks",
|
||||
"pallet-collator-selection/runtime-benchmarks",
|
||||
"pallet-message-queue/runtime-benchmarks",
|
||||
"pallet-multisig/runtime-benchmarks",
|
||||
"pallet-timestamp/runtime-benchmarks",
|
||||
"pallet-utility/runtime-benchmarks",
|
||||
"pallet-xcm-benchmarks/runtime-benchmarks",
|
||||
"pallet-xcm/runtime-benchmarks",
|
||||
"parachains-common/runtime-benchmarks",
|
||||
"polkadot-parachain-primitives/runtime-benchmarks",
|
||||
"polkadot-runtime-common/runtime-benchmarks",
|
||||
"snowbridge-core/runtime-benchmarks",
|
||||
"snowbridge-ethereum-beacon-client/runtime-benchmarks",
|
||||
"snowbridge-inbound-queue/runtime-benchmarks",
|
||||
"snowbridge-outbound-queue/runtime-benchmarks",
|
||||
"snowbridge-router-primitives/runtime-benchmarks",
|
||||
"snowbridge-system/runtime-benchmarks",
|
||||
"sp-runtime/runtime-benchmarks",
|
||||
"xcm-builder/runtime-benchmarks",
|
||||
"xcm-executor/runtime-benchmarks",
|
||||
]
|
||||
|
||||
try-runtime = [
|
||||
"asset-hub-rococo-runtime/try-runtime",
|
||||
"bridge-hub-rococo-runtime/try-runtime",
|
||||
"cumulus-pallet-aura-ext/try-runtime",
|
||||
"cumulus-pallet-dmp-queue/try-runtime",
|
||||
"cumulus-pallet-parachain-system/try-runtime",
|
||||
"cumulus-pallet-xcm/try-runtime",
|
||||
"cumulus-pallet-xcmp-queue/try-runtime",
|
||||
"frame-executive/try-runtime",
|
||||
"frame-support/try-runtime",
|
||||
"frame-system/try-runtime",
|
||||
"frame-try-runtime/try-runtime",
|
||||
"pallet-aura/try-runtime",
|
||||
"pallet-authorship/try-runtime",
|
||||
"pallet-balances/try-runtime",
|
||||
"pallet-collator-selection/try-runtime",
|
||||
"pallet-message-queue/try-runtime",
|
||||
"pallet-multisig/try-runtime",
|
||||
"pallet-session/try-runtime",
|
||||
"pallet-timestamp/try-runtime",
|
||||
"pallet-transaction-payment/try-runtime",
|
||||
"pallet-utility/try-runtime",
|
||||
"pallet-xcm/try-runtime",
|
||||
"parachain-info/try-runtime",
|
||||
"polkadot-runtime-common/try-runtime",
|
||||
"snowbridge-ethereum-beacon-client/try-runtime",
|
||||
"snowbridge-inbound-queue/try-runtime",
|
||||
"snowbridge-outbound-queue/try-runtime",
|
||||
"snowbridge-system/try-runtime",
|
||||
"sp-runtime/try-runtime",
|
||||
]
|
||||
beacon-spec-mainnet = [
|
||||
"snowbridge-ethereum-beacon-client/beacon-spec-mainnet",
|
||||
]
|
||||
experimental = ["pallet-aura/experimental"]
|
||||
|
||||
# A feature that should be enabled when the runtime should be built for on-chain
|
||||
# deployment. This will disable stuff that shouldn't be part of the on-chain wasm
|
||||
# to make it smaller like logging for example.
|
||||
on-chain-release-build = ["sp-api/disable-logging"]
|
||||
@@ -0,0 +1,94 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
|
||||
#![cfg(test)]
|
||||
|
||||
mod test_cases;
|
||||
|
||||
use asset_hub_rococo_runtime::xcm_config::bridging::to_ethereum::DefaultBridgeHubEthereumBaseFee;
|
||||
use bridge_hub_rococo_runtime::{
|
||||
xcm_config::XcmConfig, MessageQueueServiceWeight, Runtime, RuntimeEvent, SessionKeys,
|
||||
};
|
||||
use codec::Decode;
|
||||
use cumulus_primitives_core::XcmError::{FailedToTransactAsset, NotHoldingFees};
|
||||
use parachains_common::{AccountId, AuraId};
|
||||
use snowbridge_ethereum_beacon_client::WeightInfo;
|
||||
use sp_core::H160;
|
||||
use sp_keyring::AccountKeyring::Alice;
|
||||
|
||||
pub fn collator_session_keys() -> bridge_hub_test_utils::CollatorSessionKeys<Runtime> {
|
||||
bridge_hub_test_utils::CollatorSessionKeys::new(
|
||||
AccountId::from(Alice),
|
||||
AccountId::from(Alice),
|
||||
SessionKeys { aura: AuraId::from(Alice.public()) },
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn transfer_token_to_ethereum_works() {
|
||||
test_cases::send_transfer_token_message_success::<Runtime, XcmConfig>(
|
||||
collator_session_keys(),
|
||||
1013,
|
||||
1000,
|
||||
H160::random(),
|
||||
H160::random(),
|
||||
DefaultBridgeHubEthereumBaseFee::get(),
|
||||
Box::new(|runtime_event_encoded: Vec<u8>| {
|
||||
match RuntimeEvent::decode(&mut &runtime_event_encoded[..]) {
|
||||
Ok(RuntimeEvent::EthereumOutboundQueue(event)) => Some(event),
|
||||
_ => None,
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn unpaid_transfer_token_to_ethereum_fails_with_barrier() {
|
||||
test_cases::send_unpaid_transfer_token_message::<Runtime, XcmConfig>(
|
||||
collator_session_keys(),
|
||||
1013,
|
||||
1000,
|
||||
H160::random(),
|
||||
H160::random(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn transfer_token_to_ethereum_fee_not_enough() {
|
||||
test_cases::send_transfer_token_message_failure::<Runtime, XcmConfig>(
|
||||
collator_session_keys(),
|
||||
1013,
|
||||
1000,
|
||||
DefaultBridgeHubEthereumBaseFee::get() + 1_000_000_000,
|
||||
H160::random(),
|
||||
H160::random(),
|
||||
// fee not enough
|
||||
1_000_000_000,
|
||||
NotHoldingFees,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn transfer_token_to_ethereum_insufficient_fund() {
|
||||
test_cases::send_transfer_token_message_failure::<Runtime, XcmConfig>(
|
||||
collator_session_keys(),
|
||||
1013,
|
||||
1000,
|
||||
1_000_000_000,
|
||||
H160::random(),
|
||||
H160::random(),
|
||||
DefaultBridgeHubEthereumBaseFee::get(),
|
||||
FailedToTransactAsset("InsufficientBalance"),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_message_queue_service_weight_is_more_than_beacon_extrinsic_weights() {
|
||||
let max_message_queue_weight = MessageQueueServiceWeight::get();
|
||||
let force_checkpoint =
|
||||
<Runtime as snowbridge_ethereum_beacon_client::Config>::WeightInfo::force_checkpoint();
|
||||
let submit_checkpoint =
|
||||
<Runtime as snowbridge_ethereum_beacon_client::Config>::WeightInfo::submit();
|
||||
max_message_queue_weight.all_gt(force_checkpoint);
|
||||
max_message_queue_weight.all_gt(submit_checkpoint);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user