Light client bridge
Light client bridge
Light client bridge will relay every block from source chain to target chain, normally for asset transfer just need to lock asset in some backing module or smart contract in source chain and mint the mapping asset in target chain
Components
Since the chain cannot directly access each other, the cross-chain data submission needs to be completed by a third party. This third party is the bridge relayers. Anyone can become a bridge relayer, and the bridge relayer obtains income by completing the relay task between the bridges. This incentive can promote the stable existence of bridge relayers to ensure the bridge’s regular operation.
Reference Implementation
Actually Parity officially provide a poc project illustrated in poa-eth guidance
Challenges
- grandpa signature verification : too expensive
- eip 665 : not ready
Merkle Mountain Ranges
Merkle mountain ranges are just merkle trees with an efficient append operation. leaf nodes contais block header in chain,super light client not nessessary to download all the block heades,e.g. only data marked as following is required to download from full node to verify transaction in leaf index 15 is valid
MMR proof
build merkle proof of leaf node including 3 steps:
- build merkle proof of nodes from leaf to peak
- add proof of right peaks
- add proof of left peaks from right to left
MMR root
Node(p) = Blake2b(m | Node(left_child(p)) | Node(right_child(p)))
MMR in substrate
pub trait MmrApi<Hash: codec::Codec> {
/// Generate MMR proof for a leaf under given index.
#[skip_initialize_block]
fn generate_proof(leaf_index: u64) -> Result<(EncodableOpaqueLeaf, Proof<Hash>), Error>;
/// Verify MMR proof against on-chain MMR.
///
/// Note this function will use on-chain MMR root hash and check if the proof
/// matches the hash.
/// See [Self::verify_proof_stateless] for a stateless verifier.
#[skip_initialize_block]
fn verify_proof(leaf: EncodableOpaqueLeaf, proof: Proof<Hash>) -> Result<(), Error>;
/// Verify MMR proof against given root hash.
///
/// Note this function does not require any on-chain storage - the
/// proof is verified against given MMR root hash.
///
/// The leaf data is expected to be encoded in it's compact form.
#[skip_initialize_block]
fn verify_proof_stateless(root: Hash, leaf: EncodableOpaqueLeaf, proof: Proof<Hash>)
-> Result<(), Error>;
}
impl pallet_mmr::Config for Runtime {
const INDEXING_PREFIX: &'static [u8] = b"mmr";
type Hashing = Keccak256;
type Hash = <Keccak256 as traits::Hash>::Output;
type OnNewRoot = mmr_common::DepositBeefyDigest<Runtime>;
type WeightInfo = ();
type LeafData = mmr_common::Pallet<Runtime>;
}
Beefy protocol
To overcome the difficulty with GRANDPA finality proofs a separate round of BFT agreement is required where each voter will be voting on the MMR root of the latest block finalized by GRANDPA which using ECDSA for easier Ethereum compatibility and steps as following:
- listen to GRANDPA finality notifications
- finalize new blocks and start a new BEEFY round for:
last_block_with_signed_mmr_root + NextPowerOfTwo((last_finalized_block - last_block_with_signed_mmr_root) / 2)
- fetch the MMR root for the given block (currently from a header digest)
- create a BEEFY commitment where the payload is the signed MMR root for the given block
struct Commitment<BlockNumber, Payload> { //mmr root payload: Payload, block_number: BlockNumber, //from session modules validator_set_id: ValidatorSetId, }
- gossip our vote and listen for any votes for that round, waiting until received > 2⁄3.
Beefy in polkadot runtime
//start beefy(current only in rococo service)
let beefy_params = beefy_gadget::BeefyParams {
client,
backend,
key_store: keystore.clone(),
network: network.clone(),
signed_commitment_sender,
min_block_delta: 4,
prometheus_registry: prometheus_registry.clone(),
};
// Start the BEEFY bridge gadget.
task_manager.spawn_essential_handle().spawn_blocking(
"beefy-gadget",
beefy_gadget::start_beefy_gadget::<_, _, _, _>(beefy_params),
);
impl mmr_common::Config for Runtime {
type BeefyAuthorityToMerkleLeaf = mmr_common::UncompressBeefyEcdsaKeys;
type ParachainHeads = Paras;
}
Integrate MMR with Beefy
/// A BEEFY consensus digest item with MMR root hash.
pub struct DepositBeefyDigest<T>(sp_std::marker::PhantomData<T>);
impl<T> pallet_mmr::primitives::OnNewRoot<beefy_primitives::MmrRootHash> for DepositBeefyDigest<T> where
T: pallet_mmr::Config<Hash = beefy_primitives::MmrRootHash>,
T: pallet_beefy::Config,
{
fn on_new_root(root: &<T as pallet_mmr::Config>::Hash) {
let digest = sp_runtime::generic::DigestItem::Consensus(
beefy_primitives::BEEFY_ENGINE_ID,
parity_scale_codec::Encode::encode(
&beefy_primitives::ConsensusLog::<<T as pallet_beefy::Config>::BeefyId>::MmrRoot(*root)
),
);
<frame_system::Pallet<T>>::deposit_log(digest);
}
}
Snowfork Bridge
Snowbridge has a layered architecture with a clear seperation between low level bridge functionality, mid level trust functionality and high level application functionality.
Trust Layer
Ethereum MPT verification in Substrate
// Validate an Ethereum headerðash proof for import
fn validate_header_to_import(header: &EthereumHeader, proof: &[EthashProofData]) -> DispatchResult {
...
}
Beefy light client smart contract in Ethereum
Bridge Layer
guarantee basic deliverability and replay protection and with incentivized bridge adding a strict message ordering channels in both directions.
Ethereum → Substrate
There is a channel for sending Polkadot RPCs out from Ethereum to Polkadot via events. It consists of OutboundChannel contract on the Ethereum side and a corresponding InboudChannel on the parachain side, workflow as following:
Substrate → Ethereum
There is a OutboundChannel on the parachain side for sending Ethereum RPCs out from the parachain to Ethereum. It is responsible for accepting requests from other pallets and parachains for messages to be sent over to the correspongding InboundChannel smart contract
App layer and Relayer Implementation
Ethereum → Substrate Relayer is pretty straight forward so just skip and jump to Substrate → Ethereum part
start from invoking app requests (e.g lock polkadot dot asset ) into Parachain Message Commitments that will be included in the parachain header. With the help of Commitment Relayer Worker The ethereum channel then processes those commitments and verifies them via the Polkadot and Parachain Light Client Verifier to extract Ethereum RPCs. Those Ethereum RPCs are then routed to their target contract by calling that contract.
Workflow as following:
1. Following MMR Roots from Polkadot Relay Chain
The first step for trustless verification of our bridge on Ethereum starts with following the Polkadot relay chain via following new BEEFY MMR roots (as mentioned above) as they are produced and verifying their validity. Their validity is verified by checking that they are signed by the correct set of Polkadot validators.
Beefy Relayer Worker
- two phase commit
func (li *BeefyEthereumListener) pollEventsAndHeaders(ctx context.Context, descendantsUntilFinal uint64) error {
headers := make(chan *gethTypes.Header, 5)
li.ethereumConn.GetClient().SubscribeNewHead(ctx, headers)
for {
select {
case <-ctx.Done():
li.log.Info("Shutting down listener...")
return ctx.Err()
case gethheader := <-headers:
blockNumber := gethheader.Number.Uint64()
//submit initial beefy witness verification
li.forwardWitnessedBeefyJustifications()
li.processInitialVerificationSuccessfulEvents(ctx, blockNumber)
//submit CompleteSignatureCommitment
li.forwardReadyToCompleteItems(ctx, blockNumber, descendantsUntilFinal)
li.processFinalVerificationSuccessfulEvents(ctx, blockNumber)
}
}
}
case msg := <-wr.beefyMessages:
switch msg.Status {
case store.CommitmentWitnessed:
err := wr.WriteNewSignatureCommitment(ctx, msg)
if err != nil {
wr.log.WithError(err).Error("Error submitting message to ethereum")
}
case store.ReadyToComplete:
err := wr.WriteCompleteSignatureCommitment(ctx, msg)
if err != nil {
wr.log.WithError(err).Error("Error submitting message to ethereum")
}
}
}
Beefy light client smart contract
2. Applying New Relay Chain MMR Updates
These verified relay chain MMR updates contain validator set updates and parachain header updates. Then use them to update knowledge about Polkadot validators and to extract and follow new headers of Snowbridge parachain blocks.
3. Bridge Messages Verification
Lastly, with these verified parachain blocks, using a Parachain light client with the previous Parachain Commitments to verify individual bridge messages.