Hey Karp, Nam!
In Nomad, the Home contract hashes each message blob to incrementally build a merkle tree. We can put anything we want into that blob!
My understanding of Nomad is that you send messages through a Home contract. Each message sent to the Home contract includes a domain
code, which determines the merkle root to which the message should be added. Replica contracts on other chains allow users to sign and attest merkle root updates for a particular domain. In this way, the Home contract has no idea who is replicating its state. (@anna-carroll correct me if I’m wrong :))
The Home contract’s lack of knowledge of the receiver address is why I went with the 1-to-n approach. The receivers are aware of the relayer, but not vice-versa.
Yes, they roughly similar. The Nomad domains
were excluded because they’re specific to Nomad. The CrossChainReceivers
are simply executing Calls that are passed into the CrossChainRelayers
. In a way, the receivers are like an extension of the relayer.
One problem with this approach is that the Relayer contract can only determine the message destination based on either the sender address or some configuration in storage. We lost the ability to target a particular domain or target address. A Relayer contract can’t practically serve more than one message “channel”. We’d need to deploy a new Relayer if we changed the bridge.
Message handling is the big difference between Nomad and, for example, the Polygon bridge. Nomad has domains
(broadcast 1-to-n), while Polygon has a target contract address (1-to-1 messages). Both of them require additional “magic values” (i.e. data from off-chain), whether it’s the Nomad domain
or the address
of a contract on another chain in the case of Polygon.
Allowing Relayers to Send to More than One Receiver
Perhaps it’s worth defining a channel
argument that allows users to include implementation-specific data. Something like:
interface ICrossChainRelayer {
function relay(bytes calldata channel, Call[] calldata calls) external;
}
The “channel” could be the target contract address or Nomad domain, it just depends on the type of bridge. Now a single Relayer contract can be used for multiple “channels”.
Receiving Bridged Messages vs Executing
The Receiver is also not ideal, because it both listens to the bridge and executes the calls. If we need to swap the bridge, then the contract executing the calls changes. We’d need to update any Ownables or other permissions.
To make the Receiver more swappable, we could separate the bridging code from execution. Imagine we had the developer supply an Executor contract:
interface ICrossChainExecutor {
function execute(address from, Call[] calldata calls) onlyReceiver;
}
The developer would implement an executor and bind it to a particular receiver. To swap bridges they just change the receiver that the executor is bound to. The from
is the original “from” address on the sending chain; I just added that.
Thoughts? It’s late but I wanted to get some ideas out there; hopefully this makes sense! There are lots of ways we can tackle this.