Bridge v2 is the second version of bridge which is used for validators and relayers to listen to events triggered from Ronin or other chains connected to Ronin (eg: Ethereum). There are two mains features:
- Voting and relaying bridge operators (it currently changes in every period -
1 day
). - Listen to deposit and withdraw request on Ronin/Ethereum and relay it to the others.
- Ronin RPC URL is used to listen/trigger events from Ronin chain
- Ethereum RPC URL (Alchemy, Infura, etc.) is used to listen/trigger events from Ethereum
- Postgres DB to store events and tasks
There is a docker-compose.yaml
file in docker
directory. Modify .env
file and run bridge
with the following command
docker-compose -f docker/docker-compose.yaml --env-file .env up -d
Install PostgreSQL database, create bridge
database
build and install bridge
make bridge
then run Bridge with
bridge --config <path-to-config>
The config file can be found in the config
directory. There are 2 main components in the configuration: listeners and database
List all chains that Bridge is listening to. Each name reflects a specific function defined here.
For example Ronin
reflects with function InitRonin
Therefore, do not change the name, otherwise, the program cannot run properly.
- type:
hex string
Chain's identity (ronin: 0x7e4, ethereum: 0x1)
- type:
string
RPC URL of chain which is used to query new events or submit transactions to.
- type:
number
- unit:
seconds
The time of a new block is generated which is used periodically to listen to new events from the new block,
- type:
number
- unit:
blocks
Safe block range which guarantees that reorg cannot happen.
- type:
number
Maximum number of pending/processing tasks queried from the database
- type:
number
- unit:
seconds
Period of checking whether a transaction is mined or not by querying its transaction's receipt. If a receipt is found, it will try 3 more times to ensure the transaction is not replaced because of reorg.
- type:
object
Stores private key of validator and relayer. These fields can be empty and passed via environment variables
through 2 variables: RONIN_VALIDATOR_KEY
, RONIN_RELAYER_KEY
and Ethereum are: ETHEREUM_VALIDATOR_KEY
, ETHEREUM_RELAYER_KEY
If KMS is used, set these environments
RONIN_VALIDATOR_KMS_KEY_TOKEN_PATH
RONIN_VALIDATOR_KMS_SSL_CERT_PATH
RONIN_VALIDATOR_KMS_SERVER_ADDR
RONIN_VALIDATOR_KMS_SOURCE_ADDR
RONIN_VALIDATOR_KMS_SIGN_TIMEOUT
Replace RONIN
with ETHEREUM
, VALIDATOR
with RELAYER
when needed
This is the sample secret's config
{
"secret": {
"validator": {
"kmsConfig": {
"keyTokenPath": "./key",
"sslCertPath": "./ssl.crt",
"kmsServerAddr": "127.0.0.1:1234",
"kmsSourceAddr": ":5000",
"signTimeout": 3000
}
},
"relayer": {
"plainPrivateKey": "" // Raw private key
}
}
}
- type:
number
- unit:
blocks
Initially, Bridge uses this property to load data from this block. After that, Bridge will store the latest processed block in processed_block
table and use the value from this table to continue.
- type:
number
- unit:
blocks
This property guarantees that Bridge does not process too far. Specifically, when latestBlock - processWithinBlocks > fromHeight
, bridge latestBlock - processWithinBlocks
instead of fromHeight
to process.
- type:
record<string, address>
Stores a map (pair) of names and contact addresses, which can be used during processing tasks or jobs of a listener. For example, in Ronin
listener, 2 contracts which are Ronin Gateway contract (at Gateway
) and Ethereum Gateway contract (at EthGateway
) are used:
{
"Gateway": "0x03d1F13c7391F6B5A651143a31034cf728A93694",
"EthGateway": "0x3b6371EB912bFd5C0E249A16000ffbC6B881555A"
}
- type:
object
Includes all subscriptions bridge is observing in a listener. Each subscription contains the subscription name and subscription config.
to
: Indicates receiver/contract address that bridge uses as one of conditions to trigger a subscriptiontype
: There are 2 types,0
istransaction event
and1
islog's event
handler
: Define contract and event that we want to listencontract
: Contract name. This must be defined on repo Bridge Contractsname
: The event name
callbacks
: List all callbacks function when data is decoded. This is a map (pair) where the key is listener's name and value is the function that is called in that listener. For example:
{
"to": "0xA8D61A5427a778be28Bd9bb5990956b33385c738",
"type": 1, // Listen log's event
"handler": {
"contract": "RoninGateway",
"name": "MainchainWithdrew"
},
"callbacks": {
"Ronin": "StoreMainchainWithdrawCallback"
}
}
Bridge will trigger the function StoreMainchainWithdrawCallback
in RoninListener
For example, Bridge will listen to event Welcomed
which is defined on contract Hello
and submit the data to HelloEth
contract via method SubmitFromRonin
{
"MainchainWithdrewSubscription": {
"to": "0xA8D61A5427a778be28Bd9bb5990956b33385c738",
"type": 1,
"handler": {
"contract": "RoninGateway", // The contract name, it must be defined on [Bridge Contracts](https://github.com/axieinfinity/bridge-contracts/blob/master/main.go#L13-L20) first.
"name": "Welcome" // The event is listening
},
"callbacks": {
"Ronin": "WelcomeCallback" // Execute the callback on Ronin chain
}
},
}
On litenser/ronin.go
add the following method
package listener
func (l *RoninListener) WelcomeCallback(fromChainId *big.Int, tx bridgeCore.Transaction, data []byte) error {
// Unpack event data
roninEvent := new(hello.WelcomeEvent)
roninGatewayAbi, err := hello.GatewayMetaData.GetAbi()
// Since the data argument was the log's marshalled in bytes, so it must be unmarshalled
// before being use
if err = l.utilsWrapper.UnpackLog(*roninGatewayAbi, roninEvent, "Welcome", data); err != nil {
return err
}
chainId, err := l.GetChainID()
// Create a new task
t := &models.Task{
ChainId: hexutil.EncodeBig(chainId),
FromChainId: hexutil.EncodeBig(fromChainId),
FromTransaction: tx.GetHash().Hex(),
Type: task.WELCOME_TASK, // defined in task/main.go
Data: common.Bytes2Hex(data),
Retries: 0,
Status: task.STATUS_PENDING,
LastError: "",
CreatedAt: time.Now().Unix(),
}
// Get the store API to save the task to database
return l.bridgeStore.GetTaskStore().Save(withdrawalTask)
}
Then create a method welcomeTask
in task/task.go
package task
func (r *task) welcomeTask(task *models.Task) (doneTasks, processingTasks, failedTasks []*models.Task, tx *ethtypes.Transaction) {
// create caller
transactor, err := helloEth.NewHelloEth(common.HexToAddress(r.contracts[HELLO_ETH_CONTRACT]), r.ethClient)
tx, err = r.util.SendContractTransaction(r.listener.GetValidatorSign(), r.chainId, func(opts *bind.TransactOpts) (*ethtypes.Transaction, error) {
return transactor.SubmitFromRonin(opts)
})
doneTasks = append(doneTasks, task)
return
}
Finally, add it to send
method:
package task
func (r *task) send() {
log.Info("[task] Sending transaction", "type", r.taskType)
switch r.taskType {
case VOTE_BRIDGE_OPERATORS_TASK:
r.sendTransaction(r.voteBridgeOperatorsBySignature)
case RELAY_BRIDGE_OPERATORS_TASK:
r.sendTransaction(r.relayBridgeOperators)
}
}
graph TD
tryBulkAcknowledgeMainchainWithdrew --> |Emit| MainchainWithdrew
Bridge --> |Listen| MainchainWithdrew
Bridge --> A{Got event?}
A --> |No| Bridge
A --> |Yes| B[Store Receipt To Database]
Request validators sign withdrawal transaction
graph TD
bulkRequestWithdrawalFor --> |Emit| WithdrawalRequested
Bridge --> |Listen| WithdrawalRequested
Bridge --> A{Got Event?}
A --> |No| Bridge
A --> |Yes| signWithdrawalSignatures
signWithdrawalSignatures --> BulkSubmitWithdrawalSignatures
Request validators sign withdrawal transaction again
graph TD
requestWithdrawalSignatures --> |Emit| WithdrawalSignaturesRequested
Bridge --> |Listen| WithdrawalRequested
Bridge --> A{Got Event?}
A --> |No| Bridge
A --> |Yes| signWithdrawalSignatures
signWithdrawalSignatures --> BulkSubmitWithdrawalSignatures
When a user deposit ETH on Ethereum to contract. Bridge will listen this event and send it to Ronin
graph TD
requestDepositFor --> |Emit| DepositRequested
Bridge --> |Listen| DepositRequested
Bridge --> A{Got event?}
A --> |No| Bridge
A --> |Yes| sendDepositTransaction
sendDepositTransaction --> B{Is Voted?}
B --> |Yes| Done
B --> |No| StoreProcessedReceipt
StoreProcessedReceipt --> TryBulkDepositFor
When a user withdraw ETH from contract. Bridge will listen this event and send it to Ronin
graph TD
unlockWithdrawal --> |Emit| Withdrew
submitWithdrawal --> |Emit| Withdrew
Bridge --> |Listen| Withdrew
Bridge --> A{Got event?}
A --> |No| Bridge
A --> |Yes| MainchainWithdrewVoted
MainchainWithdrewVoted --> B{Is Voted?}
B --> |Yes| Done
B --> |No| TryBulkAcknowledgeMainchainWithdrew
At the end of each period, validators call wrapUpEpoch
of ValidatorSet
contract to update list validator set.
It emits an event BridgeOperatorSetUpdated(uint256 period, []address operators)
. All trusted nodes must listen this
event, vote by signing typed data and submit it to RoninGovernanceAdmin
contract.
graph TD
Validator -->|Call| WrapUpEpoch
WrapUpEpoch -->|Emit| BridgeOperatorSetUpdated
Bridge --> |Listen| BridgeOperatorSetUpdated
Bridge --> E{Got an event}
E --> |Yes| BridgeOperatorsVoted
E --> |No| Bridge
BridgeOperatorsVoted --> A{Submitted}
A --> |No| VoteBridgeOperatorsBySignatures
A --> |Yes| Done
After trusted nodes submitted vote's signature to RoninGovernanceAdmin
. Relayer needs to
call GetAllTrustedOrganizations
to get all trusted nodes, sort it as ascending. Then call GetBridgeOperatorVotingSignatures
to get a list signatures that submitted on Ronin. Finally, relayer submits these signatures to
MainchainGovernanceAdmin
through RelayBridgeOperators
method.
graph TD
VoteBridgeOperatorsBySignatures --> |Emit| BridgeOperatorsApproved
Bridge --> |Listen| BridgeOperatorsApproved
Bridge --> A{Got event}
A --> |Yes| GetAllTrustedOrganizations
A --> |No| Bridge
GetAllTrustedOrganizations --> GetBridgeOperatorVotingSignatures
GetBridgeOperatorVotingSignatures --> RelayBridgeOperators
Database configuration is defined within the key database
. Basic properties include host, port, user, password and dbName.
{
"database": {
"host": "localhost", // Database host name
"user": "postgres", // Database username
"password": "example", // Database password
"dbName": "bridge", // Database name
"port": 5432 // Database port
}
}
Thank you for considering helping out with the source code! We welcome contributions from anyone on the internet, and are grateful for even the smallest of fixes!
If you'd like to contribute to Bridge, please fork, fix, commit and send a pull request for the maintainers to review and merge into the main code base. If you wish to submit more complex changes though, please check up with the core devs first issues on Github to ensure those changes are in line with the general philosophy of the project and/or get some early feedback which can make both your efforts much lighter as well as our review and merge procedures quick and simple.
Please make sure your contributions adhere to our coding guidelines:
- Code must adhere to the official Go formatting guidelines (i.e. uses gofmt).
- Code must be documented adhering to the official Go commentary guidelines.
- Pull requests need to be based on and opened against the
master
branch. - Commit messages should be prefixed with the package(s) they modify.
- E.g. "fix: make trace configs optional"
Please see the Developers' Guide for more details on configuring your environment, managing project dependencies, and testing procedures.
Bridge library and binaries (i.e. all code outside and inside of the cmd
directory) is licensed under the
Apache License 2.0,
also included in our repository in the COPYING
file.