Darwinia Bridge


Theory

White Paper

Principle

Darwinia-Ethereum Bridge

Projects Navigation

Darwinia Bridge

cross-chain overview

Background

ERC223

ERC721

Patricia Merkle Trees

Mining difficulty calculate

mmr

relay game

Bridger

Relayers (aka. Bridgers) in Darwinia Network are offchain worker clients which help relay the headers and messages between source chains and target chains

Background

actix

deprecated dj

Shadow

Services for bridger which retrieve header data from public chains and generate mmr proof

Background

ffi

Wormhole

Background

wiki

WorkFlow && Core Logic

Ethereum To Darwinia

wormhole

invoke token contract

function redeemTokenEth(account, params, callback) {
    let web3js = new Web3(window.ethereum || window.web3.currentProvider);
    const contract = new web3js.eth.Contract(TokenABI, config[`${params.tokenType.toUpperCase()}_ETH_ADDRESS`]);
    contract.methods.transferFrom(account, config['ETHEREUM_DARWINIA_ISSUING'], params.value, params.toHex).send({ from: account }).on('transactionHash', (hash) => {
        callback && callback(hash)
    })
}

token

issuing-burn

contract

burn token and send BurnAndRedeem event

function tokenFallback(
    address _from,
    uint256 _amount,
    bytes _data
) public whenNotPaused {
    ...

    IBurnableERC20(msg.sender).burn(address(this), _amount);
    emit BurnAndRedeem(msg.sender, _from, _amount, _data);
}

bridger

scan ethereum txs from token contracts

if l.topics.contains(&contracts.ring) || l.topics.contains(&contracts.kton)
{
    EthereumTransaction {
        tx_hash: EthereumTransactionHash::Token(
            l.transaction_hash.unwrap_or_default(),
        ),
        block_hash: l.block_hash.unwrap_or_default(),
        block,
        index,
    }
} 
...       

affirm

/// Ethereum EthereumRelayHeaderParcel including ethereum block header and mmr root from shadow
#[derive(Encode, Decode, Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct EthereumRelayHeaderParcel {
	/// Ethereum header
	pub header: EthereumHeader,
	/// MMR root
	pub mmr_root: [u8; 32],
}
...
let parcel = shadow.parcel(target as usize + 1).await.with_context(|| {
    format!(
        "Fail to get parcel from shadow when affirming ethereum block {}",
        target
    )
})?;
...
let ex = Extrinsic::Affirm(parcel);
let msg = MsgExtrinsic(ex);
extrinsics_service.send(msg).await?;

redeem

/// Ethereum ReceiptProofThing including Merkle Patricia Trie proof from ethereum and mmr proof from shadow
#[derive(Clone, Debug, Default, PartialEq, Eq, Encode)]
pub struct EthereumReceiptProofThing {
	/// Ethereum Header
	pub header: EthereumHeader,
	/// Ethereum Receipt Proof
	pub receipt_proof: EthereumReceiptProof,
	/// MMR Proof
	pub mmr_proof: MMRProof,
}
...
let proof = shadow
    .receipt(&format!("{:?}", tx.enclosed_hash()), last_confirmed)
    .await?;
let redeem_for = match tx.tx_hash {
    EthereumTransactionHash::Token(_) => RedeemFor::Token,
};
...
let ex = Extrinsic::Redeem(redeem_for, proof, tx);
let msg = MsgExtrinsic(ex);
extrinsics_service.send(msg).await?;

guard vote_pending_relay_header_parcel

if pending_block_number > last_confirmed
    && !ethereum2darwinia.has_voted(&guard_account, voting_state)
{
    let parcel_from_shadow = shadow.parcel(pending_block_number as usize).await?;
    let ex = if pending_parcel.is_same_as(&parcel_from_shadow) {
        Extrinsic::GuardVote(pending_block_number, true)
    } else {
        Extrinsic::GuardVote(pending_block_number, false)
    };
    extrinsics_service.send(MsgExtrinsic(ex)).await?;
}

shadow

mmr root

fn get_mmr_root(&self, leaf_index: u64) -> Result<Option<String>> {
    ...
    let mmr_size = cmmr::leaf_index_to_mmr_size(leaf_index);
    let mmr = MMR::<[u8; 32], MergeHash, _>::new(mmr_size, store);
    let root = mmr.get_root()?;
}

mmr proof

fn gen_proof(&self, member: u64, last_leaf: u64) -> Result<Vec<String>> {
    let mmr_size = cmmr::leaf_index_to_mmr_size(last_leaf);
    let mmr = MMR::<[u8; 32], MergeHash, _>::new(mmr_size, store);
    let proof = mmr.gen_proof(vec![cmmr::leaf_index_to_pos(member)])?;
}

darwinia relay game

RelayerGame Rule

RelayerGame Protocol

start game

#[weight = 0]
pub fn affirm(
origin,
ethereum_relay_header_parcel: EthereumRelayHeaderParcel,
optional_ethereum_relay_proofs: Option<EthereumRelayProofs>
) {
  let game_id = T::RelayerGame::affirm(
  &relayer,
  ethereum_relay_header_parcel,
  optional_ethereum_relay_proofs
  )?;
  ...
}

challenge&extend game

pub fn dispute_and_affirm(
    origin,
    ethereum_relay_header_parcel: EthereumRelayHeaderParcel,
    optional_ethereum_relay_proofs: Option<EthereumRelayProofs>
) 

fn extend_affirmation(
    origin,
    extended_ethereum_relay_affirmation_id: RelayAffirmationId<EthereumBlockNumber>,
    game_sample_points: Vec<EthereumRelayHeaderParcel>,
    optional_ethereum_relay_proofs: Option<Vec<EthereumRelayProofs>>,
) 

update games

find the relayer win and confirm_relay_header_parcels

pub fn update_games(game_ids: Vec<RelayHeaderId<T, I>>) -> DispatchResult {
    let now = <frame_system::Module<T>>::block_number();
    let mut relay_header_parcels = vec![];

    for game_id in game_ids {
        trace!(
            target: "relayer-game",
            ">  Trying to Settle Game `{:?}`", game_id
        );

        let round_count = Self::round_count_of(&game_id);
        let last_round = if let Some(last_round) = round_count.checked_sub(1) {
            last_round
        } else {
            // Should never enter this condition
            error!(target: "relayer-game", "   >  Rounds - EMPTY");

            continue;
        };
        let mut relay_affirmations = Self::affirmations_of_game_at(&game_id, last_round);

        match (last_round, relay_affirmations.len()) {
            // Should never enter this condition
            (0, 0) => error!(target: "relayer-game", "   >  Affirmations - EMPTY"),
            // At first round and only one affirmation found
            (0, 1) => {
                trace!(target: "relayer-game", "   >  Challenge - NOT EXISTED");

                if let Some(relay_header_parcel) =
                    Self::settle_without_challenge(relay_affirmations.pop().unwrap())
                {
                    relay_header_parcels.push(relay_header_parcel);
                }
            }
            // No relayer response for the latest round
            (_, 0) => {
                trace!(target: "relayer-game", "   >  All Relayers Abstain, Settle Abandon");

                Self::settle_abandon(&game_id);
            }
            // No more challenge found at latest round, only one relayer win
            (_, 1) => {
                trace!(target: "relayer-game", "   >  No More Challenge, Settle With Challenge");

                if let Some(relay_header_parcel) =
                    Self::settle_with_challenge(&game_id, relay_affirmations.pop().unwrap())
                {
                    relay_header_parcels.push(relay_header_parcel);
                } else {
                    // Should never enter this condition

                    Self::settle_abandon(&game_id);
                }
            }
            (last_round, _) => {
                let distance = T::RelayableChain::distance_between(
                    &game_id,
                    Self::best_confirmed_header_id_of(&game_id),
                );

                if distance == round_count {
                    trace!(target: "relayer-game", "   >  A Full Chain Gave, On Chain Arbitrate");

                    // A whole chain gave, start continuous verification
                    if let Some(relay_header_parcel) = Self::on_chain_arbitrate(&game_id) {
                        relay_header_parcels.push(relay_header_parcel);
                    } else {
                        Self::settle_abandon(&game_id);
                    }
                } else {
                    trace!(target: "relayer-game", "   >  Still In Challenge, Update Games");

                    // Update game, start new round
                    Self::update_game_at(&game_id, last_round, now);

                    continue;
                }
            }
        }

        Self::game_over(game_id);
    }

    // TODO: handle error
    let _ = T::RelayableChain::try_confirm_relay_header_parcels(relay_header_parcels);

    trace!(target: "relayer-game", "---");

    Ok(())
}

verify proof in game

fn verify_relay_proofs(
		relay_header_id: &Self::RelayHeaderId,
		relay_header_parcel: &Self::RelayHeaderParcel,
		relay_proofs: &Self::RelayProofs,
		optional_best_confirmed_relay_header_id: Option<&Self::RelayHeaderId>,
	) -> DispatchResult {
		
    //verify ethash_proof
    ensure!(
        Self::verify_header(header, ethash_proof),
        <Error<T>>::HeaderInv
    );
    ...
    //verify mmr
    ensure!(
        Self::verify_mmr(
            last_leaf,
            mmr_root,
            mmr_proof
                .iter()
                .map(|h| array_unchecked!(h, 0, 32).into())
                .collect(),
            vec![(
                *best_confirmed_block_number,
                best_confirmed_block_header_hash
            )],
        ),
        <Error<T>>::MMRInv
    );
    ...
}

confirm relay parcel after game

pub fn update_confirmeds_with_reason(
    relay_header_parcel: EthereumRelayHeaderParcel,
    reason: Vec<u8>,
) {
    let relay_block_number = relay_header_parcel.header.number;

    ConfirmedBlockNumbers::mutate(|confirmed_block_numbers| {
        // TODO: remove old numbers according to `ConfirmedDepth`

        confirmed_block_numbers.push(relay_block_number);

        BestConfirmedBlockNumber::put(relay_block_number);
    });
    ConfirmedHeaderParcels::insert(relay_block_number, relay_header_parcel);

    Self::deposit_event(RawEvent::PendingRelayHeaderParcelConfirmed(
        relay_block_number,
        reason,
    ));
}

darwinia backing module

redeem will happen only after game finished and block confirmed

redeem

/// Redeem balances
///
/// # <weight>
/// - `O(1)`
/// # </weight>
#[weight = 10_000_000]
pub fn redeem(origin, act: RedeemFor, proof: EthereumReceiptProofThing<T>) {
    let redeemer = ensure_signed(origin)?;

    if RedeemStatus::get() {
        match act {
            RedeemFor::Token => Self::redeem_token(&redeemer, &proof)?,
            RedeemFor::Deposit => Self::redeem_deposit(&redeemer, &proof)?,
        }
    } else {
        Err(<Error<T>>::RedeemDis)?;
    }
}

parse BurnAndRedeem event

fn parse_token_redeem_proof(
		proof_record: &EthereumReceiptProofThing<T>,
	) -> Result<(T::AccountId, (bool, Balance), RingBalance<T>), DispatchError> {
		let verified_receipt = T::EthereumRelay::verify_receipt(proof_record)
			.map_err(|_| <Error<T>>::ReceiptProofInv)?;
		let fee = T::EthereumRelay::receipt_verify_fee();
		let result = {
			let eth_event = EthEvent {
				name: "BurnAndRedeem".to_owned(),
				...
		debug::trace!(target: "ethereum-backing", "[ethereum-backing] Darwinia Account: {:?}", darwinia_account);

		Ok((darwinia_account, (is_ring, redeemed_amount), fee))
}

finish the transfer

C::transfer(
    &Self::account_id(),
    &darwinia_account,
    redeem_amount,
    KeepAlive,
)?;
// // Transfer the fee from redeemer.
// T::RingCurrency::transfer(redeemer, &T::EthereumRelay::account_id(), fee, KeepAlive)?;

VerifiedProof::insert(tx_index, true);

Darwinia To Ethereum

wormhole

lock token

async function ethereumBackingLockDarwinia(account, params, callback, t) {
    try {
        console.log('ethereumBackingLock', { account, params, callback }, params.ring.toString(), params.kton.toString())
        if (window.darwiniaApi) {
            await window.darwiniaApi.isReady;
            const injector = await web3FromAddress(account);
            window.darwiniaApi.setSigner(injector.signer);
            const hash = await window.darwiniaApi.tx.ethereumBacking.lock(params.ring, params.kton, params.to)
            .signAndSend(account);
            callback && callback(hash);
        }
    } catch (error) {
        console.log(error);
    }
}

darwinia backing module

lock balance and send ScheduleMMRRootEvent

// Lock some balances into the module account
/// which very similar to lock some assets into the contract on ethereum side
///
/// This might kill the account just like `balances::transfer`
#[weight = 10_000_000]
pub fn lock(
    origin,
    #[compact] ring_to_lock: RingBalance<T>,
    #[compact] kton_to_lock: KtonBalance<T>,
    ethereum_account: EthereumAddress,
) {
    ...
    T::RingCurrency::transfer(
        &user, &Self::account_id(),
        ring_to_lock,
        AllowDeath
    )?;
  
    let raw_event = RawEvent::LockRing(
        user.clone(),
        ethereum_account.clone(),
        RingTokenAddress::get(),
        ring_to_lock
    );
    let module_event: <T as Config>::Event = raw_event.clone().into();
    let system_event: <T as frame_system::Config>::Event = module_event.into();
  
    locked = true;
  
    <LockAssetEvents<T>>::append(system_event);
    Self::deposit_event(raw_event);
    if locked {
        //will send ScheduleMMRRootEvent 
        T::EcdsaAuthorities::schedule_mmr_root((
            <frame_system::Module<T>>::block_number().saturated_into::<u32>()
                / 10 * 10 + 10
        ).saturated_into());
    }
}

bridger

handle ScheduleMMRRootEvent

// call ethereum_backing.lock will emit the event
  EventInfo::ScheduleMMRRootEvent(event) => {
      if self
          .darwinia2ethereum
          .is_authority(block, &self.account)
          .await?
      {
          info!("{}", event);
          let ex = Extrinsic::SignAndSendMmrRoot(event.block_number);
          self.delayed_extrinsics.insert(event.block_number, ex);
      }
  }

submit signed_mmr_root extrinsic

Extrinsic::SignAndSendMmrRoot(block_number) => {
    if let Some(darwinia2ethereum) = &darwinia2ethereum {
        trace!("Start sign and send mmr_root...");
        if let Some(relayer) = &darwinia2ethereum_relayer {
            let ex_hash = darwinia2ethereum
                .ecdsa_sign_and_submit_signed_mmr_root(
                    &relayer,
                    spec_name,
                    block_number,
                )
                .await?;
            info!(
                "Sign and send mmr root of block {} in extrinsic {:?}",
                block_number, ex_hash
            );
        }
    }
}

darwinia relay authorities

verify signature and send MMRRootSigned event

/// Verify
/// - the relay requirement is valid
/// - the signature is signed by the submitter
#[weight = 10_000_000]
pub fn submit_signed_mmr_root(
    origin,
    block_number: BlockNumber<T>,
    signature: RelayAuthoritySignature<T, I>
) {
    ...
    let mmr_root =
        T::DarwiniaMMR::get_root(block_number).ok_or(<Error<T, I>>::DarwiniaMMRRootNRY)?;

    ...

    ensure!(
        T::Sign::verify_signature(&signature, &message, &signer),
         <Error<T, I>>::SignatureInv
    );

    ...
    Self::mmr_root_signed(block_number);
    Self::deposit_event(RawEvent::MMRRootSigned(block_number, mmr_root, signatures));
}

wormhole

invoke VerifyProof contract

export async function ClaimTokenFromD2E({ networkPrefix, mmrIndex, mmrRoot, mmrSignatures, blockNumber, blockHeaderStr, blockHash, historyMeta} , callback, t ) {
    connect('eth', async(_networkType, _account, subscribe) => {
    
    ...
    darwiniaToEthereumVerifyProof(_account, {
        root: '0x' + historyMeta.mmrRoot,
        MMRIndex: historyMeta.best,
        blockNumber: blockNumber,
        blockHeader: blockHeader.toHex(),
        peaks: mmrProof.peaks,
        siblings: mmrProof.siblings,
        eventsProofStr: eventsProof.toHex()
    }, (result) => {
        console.log('darwiniaToEthereumVerifyProof', result)
        callback && callback(result);
    }); 
}

export async function darwiniaToEthereumVerifyProof(account, {
    root,
    MMRIndex,
    blockNumber,
    blockHeader,
    peaks,
    siblings,
    eventsProofStr
}, callback) {
    let web3js = new Web3(window.ethereum || window.web3.currentProvider);
    const contract = new web3js.eth.Contract(DarwiniaToEthereumTokenIssuingABI, config.DARWINIA_ETHEREUM_TOKEN_ISSUING);

    // bytes32 root,
    // uint32 MMRIndex,
    // uint32 blockNumber,
    // bytes memory blockHeader,
    // bytes32[] memory peaks,
    // bytes32[] memory siblings,
    // bytes memory eventsProofStr
    contract.methods.verifyProof(
        root,
        MMRIndex,
        blockHeader,
        peaks,
        siblings,
        eventsProofStr).send({ from: account }, function(error, transactionHash) {
            if(error) {
               console.log(error);
               return;
            }
            callback && callback(transactionHash)
        })
}

contract

issue mint

Issuing-Mint

function verifyProof(
        bytes32 root,
        uint32 MMRIndex,
        bytes memory blockHeader,
        bytes32[] memory peaks,
        bytes32[] memory siblings,
        bytes memory eventsProofStr
    ) 
      public
      whenNotPaused
    {
        uint32 blockNumber = Scale.decodeBlockNumberFromBlockHeader(blockHeader);

        require(!history[blockNumber], "TokenIssuing:: verifyProof:  The block has been verified");

        Input.Data memory data = Input.from(relay.verifyRootAndDecodeReceipt(root, MMRIndex, blockNumber, blockHeader, peaks, siblings, eventsProofStr, storageKey));
        
        ScaleStruct.LockEvent[] memory events = Scale.decodeLockEvents(data);

        address ring = registry.addressOf(bytes32("CONTRACT_RING_ERC20_TOKEN"));
        address kton = registry.addressOf(bytes32("CONTRACT_KTON_ERC20_TOKEN"));

        uint256 len = events.length;

        for( uint i = 0; i < len; i++ ) {
          ScaleStruct.LockEvent memory item = events[i];
          uint256 value = decimalsConverter(item.value);
          if(item.token == ring) {
            expendDailyLimit(ring, value);
            IERC20(ring).mint(item.recipient, value);

            emit MintRingEvent(item.recipient, value, item.sender);
          }

          if (item.token == kton) {
            expendDailyLimit(kton, value);
            IERC20(kton).mint(item.recipient, value);

            emit MintKtonEvent(item.recipient, value, item.sender);
          }
        }

        history[blockNumber] = true;
        emit VerifyProof(blockNumber);
    }

verify mmr

verify proof

function verifyRootAndDecodeReceipt(
    bytes32 root,
    uint32 MMRIndex,
    uint32 blockNumber,
    bytes memory blockHeader,
    bytes32[] memory peaks,
    bytes32[] memory siblings,
    bytes memory eventsProofStr,
    bytes memory key
) public view whenNotPaused returns (bytes memory){
    // verify block proof
    require(
        verifyBlockProof(root, MMRIndex, blockNumber, blockHeader, peaks, siblings),
        "Relay: Block header proof varification failed"
    );

    // get state root
    bytes32 stateRoot = Scale.decodeStateRootFromBlockHeader(blockHeader);

    return getLockTokenReceipt(stateRoot, eventsProofStr, key);
}