Skip to content
This repository has been archived by the owner on Oct 22, 2024. It is now read-only.

Commit

Permalink
SNO-305: Ethereum -> Parachain: bundle messages by address (paritytec…
Browse files Browse the repository at this point in the history
…h#699)

* SNO-324: Enable multiple accounts in outbound channel contract (paritytech#685)

* Track nonces by source address

* Remove principal from basic outbound channel

* Update relayer bindings

* Track nonces by origin address

* Fix outbound channel tests

* SNO-325: Enable multiple accounts in inbound channel pallet (paritytech#687)

* Add origin field to message decoding

* Track nonces by message origin

* Include origin address in basic channel message id

* Update basic inbound fixture data

* Add note about updating inbound channel test data

* Mention message data encoding in comment

* Update test data in envelope test

* Remove channel id from message id

* s/origin/user/g

* Swap the order of source & user

* Reword test data generation section

* s/value/nonce

* Use named fields in MessageId enum

* Switch to Twox64Concat hasher

This prevents accidental expensive lookups while remaining relatively
fast vs Blake2_128 and resistant to attacks that cause prefix
collisions, thanks to the security of keccak used to create the Ethereum
address.

* Remove rogue print statement

* SNO-326: Forward messages by address in relayer (paritytech#695)

* Remove redundant slice

* Add initial address filtering

* Remove extra nonce var

* Clean up basic channel address config

* Rename parachain relayer account config

* Remove unused method

* Fix up comments

* Add default eth addresses for basic channel

* Improve map log readability

* Fix parachain writer error messages

* Bump lodestar version in example command

* Switch to default eth account for E2E script

* Increase default timeout from 6m 40s to 25m

This helps with some of the longer running tests that are timing out
when given 400s.

* Rename mapping to nonce

* Rename origin to account in basic outbound channel

* Remove principal from outbound channel contract

* Rename user to account in basic inbound channel

* Fix event account field rename in test
  • Loading branch information
doubledup authored Oct 18, 2022
1 parent 3eddd67 commit 6e6492b
Show file tree
Hide file tree
Showing 24 changed files with 289 additions and 265 deletions.
1 change: 0 additions & 1 deletion ethereum/.envrc-example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ export INFURA_PROJECT_ID=
export ETHERSCAN_API_KEY=

# Basic Channel
export BASIC_CHANNEL_PRINCIPAL=0x0000000000000000000000000000000000000042
export BASIC_CHANNEL_SOURCE_ID=0

# Incentivized Channel
Expand Down
35 changes: 8 additions & 27 deletions ethereum/contracts/BasicOutboundChannel.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,11 @@ contract BasicOutboundChannel is OutboundChannel, ChannelAccess, AccessControl {
// Governance contracts will administer using this role.
bytes32 public constant CONFIG_UPDATE_ROLE = keccak256("CONFIG_UPDATE_ROLE");

uint64 public nonce;

// Only messages originating from this account will
// be allowed through the channel.
address public principal;
mapping(address => uint64) public nonce;

event Message(
address source,
address account,
uint64 nonce,
bytes payload
);
Expand All @@ -30,13 +27,11 @@ contract BasicOutboundChannel is OutboundChannel, ChannelAccess, AccessControl {
// Once-off post-construction call to set initial configuration.
function initialize(
address _configUpdater,
address _principal,
address[] memory defaultOperators
)
external onlyRole(DEFAULT_ADMIN_ROLE) {
// Set initial configuration
grantRole(CONFIG_UPDATE_ROLE, _configUpdater);
principal = _principal;
for (uint i = 0; i < defaultOperators.length; i++) {
_authorizeDefaultOperator(defaultOperators[i]);
}
Expand All @@ -55,29 +50,15 @@ contract BasicOutboundChannel is OutboundChannel, ChannelAccess, AccessControl {
_revokeDefaultOperator(operator);
}

// Update the principal.
function setPrincipal(address _principal) external onlyRole(CONFIG_UPDATE_ROLE) {
principal = _principal;
}

/**
* @dev Sends a message across the channel
*
* Submission is a privileged action involving two parties: The operator and the origin.
* Apps (aka operators) need to be authorized by the `origin` to submit messages via this channel.
*
* Furthermore, this channel restricts the origin to a single account, that of the principal.
* In essence this ensures that only the principal account can send messages via this channel.
*
* For pre-production testing, the restriction to the principal account can be bypassed by using
* `setPrincipal` to set the principal to the address 0x0000000000000000000000000000000000000042.
* Submission is a privileged action involving two parties: The operator and the origin (called account here).
* Apps (aka operators) need to be authorized by the `account` to submit messages via this channel.
*/
function submit(address _origin, bytes calldata _payload) external override {
require(isOperatorFor(msg.sender, _origin), "Caller is unauthorized");
if (principal != address(0x0000000000000000000000000000000000000042)) {
require(_origin == principal, "Origin is not an authorized principal");
}
nonce = nonce + 1;
emit Message(msg.sender, nonce, _payload);
function submit(address _account, bytes calldata _payload) external override {
require(isOperatorFor(msg.sender, _account), "Caller is unauthorized");
nonce[_account] = nonce[_account] + 1;
emit Message(msg.sender, _account, nonce[_account], _payload);
}
}
6 changes: 0 additions & 6 deletions ethereum/deploy/20-configure-channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ module.exports = async ({
}: HardhatRuntimeEnvironment) => {
let [deployer] = await getUnnamedAccounts();

if (!("BASIC_CHANNEL_PRINCIPAL" in process.env)) {
throw "Missing BASIC_CHANNEL_PRINCIPAL in environment config"
}
const principal = process.env.BASIC_CHANNEL_PRINCIPAL

if (!("INCENTIVIZED_CHANNEL_FEE" in process.env)) {
throw "Missing INCENTIVIZED_CHANNEL_FEE in environment config"
}
Expand Down Expand Up @@ -41,7 +36,6 @@ module.exports = async ({
},
"initialize",
deployer,
principal,
[dotApp.address, ethApp.address, erc20App.address],
);

Expand Down
9 changes: 4 additions & 5 deletions ethereum/test/test_basic_outbound_channel.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const BasicOutboundChannel = artifacts.require("BasicOutboundChannel");

const Web3Utils = require("web3-utils");
const ethers = require("ethers");
const BigNumber = web3.BigNumber;

Expand All @@ -26,8 +25,7 @@ describe("BasicOutboundChannel", function () {
describe("send", function () {
beforeEach(async function () {
this.channel = await BasicOutboundChannel.new();
const principal = "0x0000000000000000000000000000000000000042"
await this.channel.initialize(owner, principal, [appAddress]).should.be.fulfilled;
await this.channel.initialize(owner, [appAddress]).should.be.fulfilled;
});

it("should send messages out with the correct event and fields", async function () {
Expand All @@ -38,9 +36,10 @@ describe("BasicOutboundChannel", function () {
).should.be.fulfilled;

const log = tx.receipt.rawLogs[0];
const event = iface.decodeEventLog('Message(address,uint64,bytes)', log.data, log.topics);
const event = iface.decodeEventLog('Message(address,address,uint64,bytes)', log.data, log.topics);

log.address.should.be.equal(this.channel.address);
event.account.should.be.equal(origin)
event.source.should.be.equal(appAddress);
event.nonce.eq(ethers.BigNumber.from(1)).should.be.true;
event.payload.should.be.equal(testPayload)
Expand All @@ -66,7 +65,7 @@ describe("BasicOutboundChannel", function () {
).should.be.fulfilled;

const log = tx3.receipt.rawLogs[0];
const event = iface.decodeEventLog('Message(address,uint64,bytes)', log.data, log.topics);
const event = iface.decodeEventLog('Message(address,address,uint64,bytes)', log.data, log.topics);
event.nonce.eq(ethers.BigNumber.from(3)).should.be.true;
});

Expand Down
26 changes: 26 additions & 0 deletions parachain/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,32 @@ For an example configuration, consult the [setup script](/~https://github.com/Snow

To run the parachain tests locally, use `cargo test --release`. For the full suite of tests, use `cargo test --release --features runtime-benchmarks`.

### 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,incentivized}_outbound_channel.js`.
For each encoded log you want to create, find a transaction object `tx` returned from a `submit` call, then run this:

```javascript
const rlp = require("rlp");
const rawLog = tx.receipt.rawLogs[0];
const encodedLog = rlp.encode([rawLog.address, rawLog.topics, rawLog.data]).toString("hex");
console.log(`encodedLog: ${encodedLog}`);
```

To decode the event from a `rawLog` to inspect the event data:

```javascript
const iface = new ethers.utils.Interface(BasicOutboundChannel.abi);
const decodedEventLog = iface.decodeEventLog(
'Message(address,address,uint64,bytes)',
rawLog.data,
rawLog.topics,
);
console.log(`decoded rawLog.data: ${JSON.stringify(decodedEventLog)}`);
```

Set the contract object and event signature based on the log you want to decode.

## Chain metadata

There is an internal tool `snowbridge-query-events` which is used to read specific events from the parachain. It is a used by our offchain message relayers.
Expand Down
45 changes: 25 additions & 20 deletions parachain/pallets/basic-channel/src/inbound/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ use sp_std::{convert::TryFrom, prelude::*};

// Used to decode a raw Ethereum log into an [`Envelope`].
static EVENT_ABI: &Event = &Event {
signature: "Message(address,uint64,bytes)",
signature: "Message(address,address,uint64,bytes)",
inputs: &[
Param { kind: ParamKind::Address, indexed: false },
Param { kind: ParamKind::Address, indexed: false },
Param { kind: ParamKind::Uint(64), indexed: false },
Param { kind: ParamKind::Bytes, indexed: false },
Expand All @@ -22,6 +23,8 @@ pub struct Envelope {
pub channel: H160,
/// The application on Ethereum where the message originated from.
pub source: H160,
/// The account on Ethereum that authorized the source to send the message.
pub account: H160,
/// A nonce for enforcing replay protection and ordering.
pub nonce: u64,
/// The inner payload generated from the source application.
Expand All @@ -44,8 +47,13 @@ impl TryFrom<Log> for Envelope {
_ => return Err(EnvelopeDecodeError),
};

let account = match iter.next().ok_or(EnvelopeDecodeError)? {
Token::Address(account) => account,
_ => return Err(EnvelopeDecodeError),
};

let nonce = match iter.next().ok_or(EnvelopeDecodeError)? {
Token::Uint(value) => value.low_u64(),
Token::Uint(nonce) => nonce.low_u64(),
_ => return Err(EnvelopeDecodeError),
};

Expand All @@ -54,7 +62,7 @@ impl TryFrom<Log> for Envelope {
_ => return Err(EnvelopeDecodeError),
};

Ok(Self { channel: log.address, source, nonce, payload })
Ok(Self { channel: log.address, account, source, nonce, payload })
}
}

Expand All @@ -63,17 +71,16 @@ mod tests {
use super::*;
use hex_literal::hex;

const LOG: [u8; 284] = hex!(
const LOG: [u8; 251] = hex!(
"
f901199430d2da52e36f80b17fe2694a5e4900b81cf26344e1a0779b38144a38
cfc4351816442048b17fe24ba2b0e0c63446b576e8281160b15bb8e000000000
0000000000000000abe98e5ef4dc7a5c4f317823986fe48649f0edbb00000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000006000000000
000000000000000000000000000000000000000000000000000000541ed28b61
269a6d3d28d07b1fd834ebe4e703368ed43593c715fdd31c61141abd04a99fd6
822c8558854ccde39a5684e7a56da27d00010000000000000000000000000000
00000000000000000000000000000000000000000000000000000000
f8f99486d9ac0bab011917f57b9e9607833b4340f9d4f8e1a0daab80e8986999
7d1cabbe1122788e90fe72b9234ff97a9217dcbb5126f3562fb8c00000000000
0000000000000089b4ab1ef20763630df9743acf155865600daff20000000000
0000000000000004e00e6d2e9ea1e2af553de02a5172120bfa5c3e0000000000
0000000000000000000000000000000000000000000000000000010000000000
0000000000000000000000000000000000000000000000000000800000000000
0000000000000000000000000000000000000000000000000000206172626974
726172792d7061796c6f6164000000000000000000000000000000
"
);

Expand All @@ -85,14 +92,12 @@ mod tests {
assert_eq!(
envelope,
Envelope {
channel: hex!["30d2da52e36f80b17fe2694a5e4900b81cf26344"].into(),
source: hex!["abe98e5ef4dc7a5c4f317823986fe48649f0edbb"].into(),
nonce: 0,
channel: hex!["86d9ac0bab011917f57b9e9607833b4340f9d4f8"].into(),
source: hex!["89b4ab1ef20763630df9743acf155865600daff2"].into(),
account: hex!["04e00e6d2e9ea1e2af553de02a5172120bfa5c3e"].into(),
nonce: 1,
payload: hex!(
"
1ed28b61269a6d3d28d07b1fd834ebe4e703368ed43593c715fdd31c61141abd
04a99fd6822c8558854ccde39a5684e7a56da27d000100000000000000000000
0000000000000000000000000000000000000000"
"6172626974726172792d7061796c6f6164000000000000000000000000000000"
)
.into(),
}
Expand Down
6 changes: 3 additions & 3 deletions parachain/pallets/basic-channel/src/inbound/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ pub mod pallet {
pub type SourceChannel<T: Config> = StorageValue<_, H160, ValueQuery>;

#[pallet::storage]
pub type Nonce<T: Config> = StorageValue<_, u64, ValueQuery>;
pub type Nonce<T: Config> = StorageMap<_, Twox64Concat, H160, u64, ValueQuery>;

#[pallet::storage]
pub type LatestVerifiedBlockNumber<T: Config> = StorageValue<_, u64, ValueQuery>;
Expand Down Expand Up @@ -108,7 +108,7 @@ pub mod pallet {
}

// Verify message nonce
<Nonce<T>>::try_mutate(|nonce| -> DispatchResult {
<Nonce<T>>::try_mutate(envelope.account, |nonce| -> DispatchResult {
if envelope.nonce != *nonce + 1 {
Err(Error::<T>::InvalidNonce.into())
} else {
Expand All @@ -117,7 +117,7 @@ pub mod pallet {
}
})?;

let message_id = MessageId::new(ChannelId::Basic, envelope.nonce);
let message_id = MessageId::Basic { account: envelope.account, nonce: envelope.nonce };
T::MessageDispatch::dispatch(envelope.source, message_id, &envelope.payload);

<LatestVerifiedBlockNumber<T>>::set(block_number);
Expand Down
64 changes: 37 additions & 27 deletions parachain/pallets/basic-channel/src/inbound/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use snowbridge_ethereum::{Header as EthereumHeader, Log, U256};

use hex_literal::hex;

use crate::{inbound as basic_inbound_channel, inbound::Error};
use crate::{inbound as basic_inbound_channel, inbound::Error, inbound::envelope::Envelope};

type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
type Block = frame_system::mocking::MockBlock<Test>;
Expand Down Expand Up @@ -118,8 +118,14 @@ pub fn new_tester_with_config(
ext
}

fn parse_origin(message: Message) -> H160 {
let (log, _) = MockVerifier::verify(&message).map_err(|err| { println!("mock verify: {:?}", err); err }).unwrap();
let envelope = Envelope::try_from(log).map_err(|err| { println!("envelope: {:?}", err); err }).unwrap();
envelope.account
}

// The originating channel address for the messages below
const SOURCE_CHANNEL_ADDR: [u8; 20] = hex!["2d02f2234d0B6e35D8d8fD77705f535ACe681327"];
const SOURCE_CHANNEL_ADDR: [u8; 20] = hex!["86d9ac0bab011917f57b9e9607833b4340f9d4f8"];

// Ethereum Log:
// address: 0xe4ab635d0bdc5668b3fcb4eaee1dec587998f4af (outbound channel contract)
Expand All @@ -128,17 +134,16 @@ const SOURCE_CHANNEL_ADDR: [u8; 20] = hex!["2d02f2234d0B6e35D8d8fD77705f535ACe68
// source: 0x8f5acf5f15d4c3d654a759b96bb674a236c8c0f3 (ETH bank contract)
// nonce: 1
// payload ...
const MESSAGE_DATA_0: [u8; 284] = hex!(
const MESSAGE_DATA_0: [u8; 251] = hex!(
"
f90119942d02f2234d0b6e35d8d8fd77705f535ace681327e1a0779b38144a38
cfc4351816442048b17fe24ba2b0e0c63446b576e8281160b15bb8e000000000
00000000000000000a42cba2b7960a0ce216ade5d6a82574257023d800000000
0000000000000000000000000000000000000000000000000000000100000000
0000000000000000000000000000000000000000000000000000006000000000
000000000000000000000000000000000000000000000000000000570c018213
dae5f9c236beab905c8305cb159c5fa1aae500d43593c715fdd31c61141abd04
a99fd6822c8558854ccde39a5684e7a56da27d0000d9e9ac2d78030000000000
00000000000000000000000000000000000000000000000000000000
f8f99486d9ac0bab011917f57b9e9607833b4340f9d4f8e1a0daab80e8986999
7d1cabbe1122788e90fe72b9234ff97a9217dcbb5126f3562fb8c00000000000
0000000000000089b4ab1ef20763630df9743acf155865600daff20000000000
0000000000000004e00e6d2e9ea1e2af553de02a5172120bfa5c3e0000000000
0000000000000000000000000000000000000000000000000000010000000000
0000000000000000000000000000000000000000000000000000800000000000
0000000000000000000000000000000000000000000000000000206172626974
726172792d7061796c6f6164000000000000000000000000000000
"
);

Expand All @@ -149,17 +154,16 @@ const MESSAGE_DATA_0: [u8; 284] = hex!(
// source: 0x8f5acf5f15d4c3d654a759b96bb674a236c8c0f3 (ETH bank contract)
// nonce: 1
// payload ...
const MESSAGE_DATA_1: [u8; 284] = hex!(
const MESSAGE_DATA_1: [u8; 251] = hex!(
"
f90119942d02f2234d0b6e35d8d8fd77705f535ace681327e1a0779b38144a38
cfc4351816442048b17fe24ba2b0e0c63446b576e8281160b15bb8e000000000
00000000000000000a42cba2b7960a0ce216ade5d6a82574257023d800000000
0000000000000000000000000000000000000000000000000000000200000000
0000000000000000000000000000000000000000000000000000006000000000
000000000000000000000000000000000000000000000000000000570c018213
dae5f9c236beab905c8305cb159c5fa1aae500d43593c715fdd31c61141abd04
a99fd6822c8558854ccde39a5684e7a56da27d0000d9e9ac2d78030000000000
00000000000000000000000000000000000000000000000000000000
f8f99486d9ac0bab011917f57b9e9607833b4340f9d4f8e1a0daab80e8986999
7d1cabbe1122788e90fe72b9234ff97a9217dcbb5126f3562fb8c00000000000
0000000000000089b4ab1ef20763630df9743acf155865600daff20000000000
0000000000000004e00e6d2e9ea1e2af553de02a5172120bfa5c3e0000000000
0000000000000000000000000000000000000000000000000000020000000000
0000000000000000000000000000000000000000000000000000800000000000
0000000000000000000000000000000000000000000000000000206172626974
726172792d7061796c6f6164000000000000000000000000000000
"
);

Expand Down Expand Up @@ -200,8 +204,10 @@ fn test_submit() {
data: Default::default(),
},
};
assert_ok!(BasicInboundChannel::submit(origin.clone(), message_1));
let nonce: u64 = <Nonce<Test>>::get();
assert_ok!(BasicInboundChannel::submit(origin.clone(), message_1.clone()));

let event_origin = parse_origin(message_1);
let nonce: u64 = <Nonce<Test>>::get(event_origin.clone());
assert_eq!(nonce, 1);

// Submit message 2
Expand All @@ -213,8 +219,10 @@ fn test_submit() {
data: Default::default(),
},
};
assert_ok!(BasicInboundChannel::submit(origin.clone(), message_2));
let nonce: u64 = <Nonce<Test>>::get();
assert_ok!(BasicInboundChannel::submit(origin.clone(), message_2.clone()));

let event_origin_2 = parse_origin(message_2);
let nonce: u64 = <Nonce<Test>>::get(event_origin_2.clone());
assert_eq!(nonce, 2);
});
}
Expand All @@ -235,7 +243,9 @@ fn test_submit_with_invalid_nonce() {
},
};
assert_ok!(BasicInboundChannel::submit(origin.clone(), message.clone()));
let nonce: u64 = <Nonce<Test>>::get();

let event_origin = parse_origin(message.clone());
let nonce: u64 = <Nonce<Test>>::get(event_origin);
assert_eq!(nonce, 1);

// Submit the same again
Expand Down
Loading

0 comments on commit 6e6492b

Please sign in to comment.