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:
Clara van Staden
2023-12-21 18:06:36 +02:00
committed by GitHub
parent 9f5221cc2f
commit 18d53dbf91
151 changed files with 19379 additions and 149 deletions
Generated
+888 -66
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -36,6 +36,20 @@ members = [
"bridges/primitives/test-utils", "bridges/primitives/test-utils",
"bridges/primitives/xcm-bridge-hub", "bridges/primitives/xcm-bridge-hub",
"bridges/primitives/xcm-bridge-hub-router", "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/cli",
"cumulus/client/collator", "cumulus/client/collator",
"cumulus/client/consensus/aura", "cumulus/client/consensus/aura",
+201
View File
@@ -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.
+127
View File
@@ -0,0 +1,127 @@
# Snowbridge &middot;
[![codecov](https://codecov.io/gh/Snowfork/snowbridge/branch/main/graph/badge.svg?token=9hvgSws4rN)]
(https://codecov.io/gh/Snowfork/snowbridge)
![GitHub](https://img.shields.io/github/license/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.
+201
View File
@@ -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.
+155
View File
@@ -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.
@@ -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(
&current_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))
}
}
@@ -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"
]
}
@@ -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"
]
}
@@ -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"
]
}
@@ -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"
]
}
@@ -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"
]
}
@@ -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(&params.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());
}
@@ -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