The peggy module enables the Injective Chain to support a trustless, on-chain bidirectional ERC-20 token bridge to Ethereum. In this system, holders of ERC-20 tokens on Ethereum can convert their ERC-20 tokens to Cosmos-native coins on the Injective Chain and vice-versa.
This decentralized bridge is secured and operated by the validators of the Injective Chain.
Peggy smart contract on Ethereum
Peggy module on the Injective Chain
Peggo (off-chain relayer aka orchestrator)
Oracle (Observes events of Peggy contract and send claims to the Peggy module)
EthSigner (Signs Valset and Batch confirmations to the Peggy module)
Batch Requester (Sends batch token withdrawal requests to the Peggy module)
Valset Relayer (Submits Validator set updates to the Peggy contract)
Batch Relayer (Submits batches of token withdrawals to the Peggy contract)
In addition to running an injectived
node to sign blocks, Injective Chain validators must also run the peggo
orchestrator to relay data from the Peggy smart contract on Ethereum and the Peggy module on the Injective Chain.
Maintaining an up-to-date checkpoint of the Injective Chain validator set on Ethereum
Transferring ERC-20 tokens from Ethereum to the Injective Chain
Transferring pegged tokens from the Injective Chain to Ethereum
This doc aims to provide an overview of Peggy
(Injective's Ethereum bridge) from a technical perspective and dive deep into its operational logic. Peggy is the name of the custom Cosmos SDK module built on Injective as well as the Ethereum contract (Peggy.sol) which make up both sides of the bridge. Connected via a middle-man process called Peggo
users can securely move token assets between networks.
To suggest improvements, please open a GitHub issue.
Words matter and we seek clarity in the terminology so we can have clarity in our thinking and communication. To help better understand, some key definitions are:
Operator
- this is a person (or people) who control and operate Validator
and Orchestrator
processes
Validator
- this is an Injective Chain validating node (eg. injectived
process)
Validator Set
- the (active) set of Injective Chain Validators
(Valset) along with their respective voting power as determined by their stake weight. Each validator is associated with an Ethereum address to be represented on the Ethereum network
Orchestrator (Peggo)
- the off-chain process (peggo
) that plays the middleman role between Injective and Ethereum. Orchestrators are responsible for keeping the bridge online and require active endpoints to fully synced Injective (Ethereum) nodes
Peggy module
- the counterparty Cosmos module for Peggy contract
. Besides providing services to bridge token assets, it automatically reflects on the active Validator Set
as it changes over time. The update is later applied on Ethereum via Peggo
Peggy Contract
- The Ethereum contract that holds all the ERC-20 tokens. It also maintains a compressed checkpointed representation of the Injective Chain Validator Set
using Delegate Keys
and normalized powers
Delegate Keys
- when an Operator
sets up their Orchestrator
for the first time they register (on Injective) their Validator
's address with an Ethereum address. The corresponding key is used to sign messages and represent that validator on Ethereum. Optionally, one delegate Injective Chain account key can be provided to sign Injective messages (eg Claims
) on behalf of the Validator
Peggy Tx pool (withdrawals)
- when a user wishes to move their asset from Injective to Ethereum their individual tx gets pooled with others with the same asset
Peggy Batch pool
- pooled withdrawals are batched together (by an Orchestrator
) to be signed off and eventually relayed to Ethereum. These batches are kept within this pool
Claim
- a signed proof (by an Orchestrator
) that an event occurred in the Peggy contract
Attestation
- an aggregate of claims for a particular event nonce emitted from Peggy contract
. After a majority of Orchestrators
attests to a claim, the event is acknowledged and executed on Injective
Majority
- the majority of Injective network, 2/3 + 1 validators
Deposit
- an asset transfer initiated from Ethereum to Injective
Withdrawal
- an asset transfer initiated from Injective to Ethereum (present in Peggy Tx pool
)
Batch
- a batch of withdrawals with the same token type (present in Peggy Batch pool
)
To recap, each Operator
is responsible for maintaining 2 secure processes:
A fully synced Injective Chain Validator
node (injectived
process)
The Orchestrator
service (peggo orchestrator
process) which interacts with both networks. Implicitly, an RPC endpoint to a fully synced Ethereum node is required as well (see peggo .env example)
Combined, these 2 entities accomplish 3 things:
Move token assets from Ethereum to Injective
Move token assets from Injective to Ethereum
Keep the Peggy.sol
contract in sync with the active Validator Set
on Injective
It is possible to run peggo
without ever being a Validator
. Peggo automatically runs in "relayer mode" when configured to run with an address not associated with a Validator
. In this mode, only 2 things can happen:
new token batches can be created on Injective
confirmed valsets/batches can be relayed to Ethereum
Any asset originating from Ethereum which implements the ERC-20 standard can be transferred from Ethereum to Injective by calling the sendToInjective
function on the Peggy.sol contract which transfers tokens from the sender's balance to the Peggy contract.
The Operators
all run their peggo
processes which submit MsgDepositClaim
messages describing the deposit they have observed. Once more than 66% of all voting power has submitted a claim for this specific deposit representative tokens are minted and issued to the Injective Chain address that the sender requested.
These representative tokens have a denomination prefix of peggy
concatenated with the ERC-20 token hex address, e.g. peggy0xdac17f958d2ee523a2206206994597c13d831ec7
.
An asset native to a Cosmos SDK chain (e.g. ATOM
) first must be represented on Ethereum before it's possible to bridge it. To do so, the Peggy contract allows anyone to create a new ERC-20 token representing a Cosmos asset by calling the deployERC20
function.
This endpoint is not permissioned, so it is up to the validators and the users of the Peggy bridge to declare any given ERC-20 token as the representation of a given asset.
When a user on Ethereum calls deployERC20
they pass arguments describing the desired asset. Peggy.sol uses an ERC-20 factory to deploy the actual ERC-20 contract and assigns ownership of the entire balance of the new token to the Peggy contract itself before emitting an ERC20DeployedEvent
.
The peggo orchestrators observe this event and decide if a Cosmos asset has been accurately represented (correct decimals, correct name, no pre-existing representation). If this is the case, the ERC-20 contract address is adopted and stored as the definitive representation of that Cosmos asset on Ethereum.
Orchestrator
(Peggo) subprocessesThe peggo orchestrator
process consists of 4 subprocesses running concurrently at exact intervals (loops). These are:
Signer
which signs new Validator Set
updates and Token Batches
with the Operator
's Ethereum keys and submits using messages.
Oracle
which observes Ethereum events and sends them as claims to Injective.
Relayer
which submits confirmed Validator Set
updates and Token Batches
to the Peggy Contract
on Ethereum
Batch Creator
which observes (new) withdrawals on Injective and decides which of these to batch according to their type and the configured PEGGO_MIN_BATCH_FEE_USD
value
The purpose of the Batch Creator
is only in creating token batches on the Injective side. The relevant Peggy module
RPC is not permissioned so anyone can create a batch.
When a user wants to withdraw assets from Injective to Ethereum they send a special message to Injective (MsgSendToEth
) which adds their withdrawal to Peggy Tx Pool
. Batch Creator
continually queries the pool for withdrawals (by token type) and issues a MsgRequestBatch
to Injective when a potential batch satisfies the configured PEGGO_MIN_BATCH_FEE_USD
value (see .env example).
On the receiving end, all pooled withdrawals matching the token type in the request are moved from the Outgoing Tx Pool
as a single batch and placed in the Outgoing Batch Pool
.
The responsibility of Signer is to provide confirmations that an Operator (Orchestrator)
is partaking in bridge activity. Failure to provide these confirmations results in slashing penalties for the orchestrator's Validator
. In other words, this process must be running at all times for a Validator
node.
Any payload moving in the Injective->Ethereum pipeline (Validator Set
updates/Token Batches
) requires Validator
signatures to be successfully relayed to Ethereum. Certain calls on Peggy Contract
accept an array of signatures to be checked against the Validator Set
in the contract itself. Orchestrators
make these signatures with their Delegate Ethereum address
: this is an Ethereum address decided by the Operator
upon initial setup (SetOrchestratorAddress). This address then represents that validator on the Ethereum blockchain and will be added as a signing member of the multisig with a weighted voting power as close as possible to the Injective Chain voting power.
Whenever Signer
finds that there is a unconfirmed valset update (token batch) present within the Peggy Module
it issues a MsgConfirmValset
(MsgConfirmBatch
) as proof that the operating Validator
is active in bridge activity.
Monitors the Ethereum network for new events involving the Peggy Contract
.
Every event emitted by the contract has a unique event nonce. This nonce value is crucial in coordinating Orchestrators
to properly observe contract activity and make sure Injective acknowledges them via Claims
. Multiple claims of the same nonce make up an Attestation
and when the majority (2/3) of orchestrators have observed an event its particular logic gets executed on Injective.
If 2/3 of the validators can not agree on a single Attestation
, the oracle is halted. This means no new events will be relayed from Ethereum until some of the validators change their votes. There is no slashing condition for this, with reasoning outlined in the slashing spec
There are 4 types of events emitted from Peggy.sol:
TransactionBatchExecutedEvent
- event indicating that a token batch (withdrawals) has been successfully relayed to Ethereum
ValsetUpdatedEvent
- event indicating that a Validator Set
update has been successfully relayed to Ethereum
SendToInjectiveEvent
- event indicating that a new deposit to Injective has been initiated
ERC20DeployedEvent
- event indicating a new Cosmos token has been registered on Ethereum
Injective's Oracle
implementation ignores the last 12 blocks on Ethereum to ensure block finality. In reality, this means latest events are observed 2-3 minutes after they occurred.
Relayer
bundles valset updates (or token batches) along with their confirmations into an Ethereum transaction and sends it to the Peggy contract
.
Keep in mind that these messages cost a variable amount of money based on wildly changing Ethereum gas prices, so it's not unreasonable for a single batch to cost over a million gas. A major design decision for our relayer rewards was to always issue them on the Ethereum chain. This has downsides, namely some strange behavior in the case of validator set update rewards.
But the upsides are undeniable, because the Ethereum messages pay msg.sender
any existing bot in the Ethereum ecosystem will pick them up and try to submit them. This makes the relaying market much more competitive and less prone to cabal like behavior.
This document describes the end to end lifecycle of the Peggy bridge.
In order to deploy the Peggy contract, the validator set of the native chain (Injective Chain) must be known. Upon deploying the Peggy contract suite (Peggy Implementation, Proxy contract, and ProxyAdmin contracts), the Peggy contract (the Proxy contract) must be initialized with the validator set. Upon initialization a ValsetUpdatedEvent
is emitted from the contract.
The proxy contract is used to upgrade Peggy Implementation contract which is needed for bug fixing and potential improvements during initial phase. It is a simple wrapper or "proxy" which users interact with directly and is in charge of forwarding transactions to the Peggy implementation contract, which contains the logic. The key concept to understand is that the implementation contract can be replaced but the proxy (the access point) is never changed.
The ProxyAdmin is a central admin for the Peggy proxy, which simplifies management. It controls upgradability and ownership transfers. The ProxyAdmin contract itself has a built-in expiration time which, once expired, prevents the Peggy implementation contract from being upgraded in the future.
Then the following peggy genesis params should be updated:
bridge_ethereum_address
with Peggy proxy contract address
bridge_contract_start_height
with the height at which the Peggy proxy contract was deployed
This completes the bootstrap of the Peggy bridge and the chain can be started. Afterward, Operators
should start their peggo
processes and eventually observe that the initial ValsetUpdatedEvent
is attested on Injective.
A validator set is a series of Ethereum addresses with attached normalized powers used to represent the Injective validator set (Valset) in the Peggy contract on Ethereum. The Peggy contract stays in sync with the Injective Chain validator set through the following mechanism:
Creating a new Valset on Injective: A new Valset is automatically created on the Injective Chain when either:
the cumulative difference of the current validator set powers compared to the last recorded Valset exceeds 5%
a validator begins unbonding from the set
Confirming a Valset on Injective: Each Operator
is responsible for confirming Valset updates that are created on Injective. The Signer
process sends these confirmations via MsgConfirmValset
by having the validator's delegated Ethereum key sign over a compressed representation of the Valset data. The Peggy module
verifies the validity of the signature and persists it to its state.
Updating the Valset on the Peggy contract: After a 2/3+ 1 majority of validators have submitted their confirmations for a given Valset, Relayer
submits the new Valset data to the Peggy contract by calling updateValset
. The Peggy contract then validates the data, updates the valset checkpoint, transfers valset rewards to sender and emits a ValsetUpdatedEvent
.
Acknowledging the ValsetUpdatedEvent
on Injective: Oracle
witnesses the ValsetUpdatedEvent
on Ethereum, and sends a MsgValsetUpdatedClaim
which informs the Peggy module
that the Valset has been updated on Ethereum.
Pruning Valsets on Injective: Once a 2/3 majority of validators send their claim for a given ValsetUpdateEvent
, all the previous valsets are pruned from the Peggy module
state.
Validator Slashing: Validators are subject to slashing after a configured window of time (SignedValsetsWindow
) for not providing confirmations. Read more valset slashing
ERC-20 tokens are transferred from Ethereum to Injective through the following mechanism:
Depositing ERC-20 tokens on the Peggy Contract: A user initiates a transfer of ERC-20 tokens from Ethereum to Injective by calling the sendToInjective
function on the Peggy contract which deposits tokens on the Peggy contract and emits a SendToInjectiveEvent
. The deposited tokens will remain locked until withdrawn at some undetermined point in the future. This event contains the amount and type of tokens, as well as a destination address on the Injective Chain to receive the funds.
Confirming the deposit: Each Oracle
witnesses the SendToInjectiveEvent
and sends a MsgDepositClaim
which contains the deposit information to the Peggy module.
Minting tokens on the Injective: Once a majority of validators confirm the deposit claim, the deposit is processed.
If the asset is Ethereum originated, the tokens are minted and transferred to the intended recipient's address on the Injective Chain.
If the asset is Cosmos-SDK originated, the coins are unlocked and transferred to the intended recipient's address on the Injective Chain.
Request Withdrawal from Injective: A user can initiate the transfer of assets from the Injective Chain to Ethereum by sending a MsgSendToEth
transaction to the peggy module.
If the asset is Ethereum native, the represented tokens are burnt.
If the asset is Cosmos SDK native, coins are locked in the module. The withdrawal is then added to Outgoing Tx Pool
.
Batch Creation: A Batch Creator
observes the pool of pending withdrawals. The batch creator (or any external third party) then requests a batch of to be created for given token by sending MsgRequestBatch
to the Injective Chain. The Peggy module
collects withdrawals matching the token type into a batch and puts it in Outgoing Batch Pool
.
Batch Confirmation: Upon detecting the existence of an Outgoing Batch, the Signer
signs over the batch with its Ethereum key and submits a MsgConfirmBatch
tx to the Peggy module.
Submit Batch to Peggy Contract: Once a majority of validators confirm the batch, the Relayer
calls submitBatch
on the Peggy contract with the batch and its confirmations. The Peggy contract validates the signatures, updates the batch checkpoint, processes the batch ERC-20 withdrawals, transfers the batch fee to the tx sender and emits a TransactionBatchExecutedEvent
.
Send Withdrawal Claim to Injective: Oracles
witness the TransactionBatchExecutedEvent
and send a MsgWithdrawClaim
containing the withdrawal information to the Peggy module.
Prune Batches Once a majority of validators submit their MsgWithdrawClaim
, the batch is deleted along and all previous batches are cancelled on the Peggy module. Withdrawals in cancelled batches get moved back into Outgoing Tx Pool
.
Batch Slashing: Validators are responsible for confirming batches and are subject to slashing if they fail to do so. Read more on batch slashing.
Note while that batching reduces individual withdrawal costs dramatically, this comes at the cost of latency and implementation complexity. If a user wishes to withdraw quickly they will have to pay a much higher fee. However this fee will be about the same as the fee every withdrawal from the bridge would require in a non-batching system.
This doc lists all the data Peggy module reads/writes to its state as KV pairs
Params is a module-wide configuration structure that stores parameters and defines overall functioning of the peggy module. Detailed specification for each parameter can be found in the Parameters section.
[]byte{0x4}
Module params
types.Params
Protobuf encoded
Stores Delegate Ethereum address
indexed by the Validator
's account address
[]byte{0x1} + []byte(validatorAddr)
Ethereum address
common.Address
Protobuf encoded
Stores Validator
account address indexed by the Delegate Ethereum address
[]byte{0xfb} + []byte(ethAddress)
Validator address
sdk.ValAddress
Protobuf encoded
When a validator would like to delegate their voting power to another key. The value is stored using the orchestrator address as the key
[]byte{0xe8} + []byte(AccAddress)
Orchestrator address assigned by a validator
[]byte
Protobuf encoded
This is the validator set of the bridge. Created automatically by Peggy module
during EndBlocker.
Stored in two possible ways, first with a height and second without (unsafe). Unsafe is used for testing and export and import of state.
[]byte{0x2} + nonce (big endian encoded)
Validator set
types.Valset
Protobuf encoded
The latest validator set slash nonce. This is used to track which validator set needs to be slashed and which already has been.
[]byte{0xf5}
Nonce
uint64
encoded via big endian
Nonce of the latest validator set. Updated on each new validator set.
[]byte{0xf6}
Nonce
uint64
encoded via big endian
Singer
confirmation for a particular validator set. See oracle messages
[]byte{0x3} + (nonce + []byte(AccAddress)
Validator Confirmation
types.MsgValsetConfirm
Protobuf encoded
Singer
confirmation for a particular token batch. See oracle messages
[]byte{0xe1} + []byte(tokenContract) + nonce + []byte(AccAddress)
Validator Batch Confirmation
types.MsgConfirmBatch
Protobuf encoded
User withdrawals are pooled together in Peggy Tx Pool
ready to be batched later by a Batch Creator
.
Each withdrawal is indexed by a unique nonce set by the Peggy module
when the withdrawal was received.
[]byte{0x7} + []byte("lastTxPoolId")
nonce of outgoing withdrawal
uint64
Big endian encoded
Monotonically increasing value for each withdrawal received by Injective
[]byte{0x6} + []byte("lastTxPoolId")
Last used withdrawal ID
uint64
Big endian encoded
OutgoingTxBatch
represents a collection of withdrawals of the same token type. Created on every successful MsgRequestBatch
.
Stored in two possible ways, first with a height and second without (unsafe). Unsafe is used for testing and export and import of state. Currently Peggy.sol is hardcoded to only accept batches with a single token type and only pay rewards in that same token type.
[]byte{0xa} + []byte(tokenContract) + nonce (big endian encoded)
A batch of outgoing transactions
types.OutgoingTxBatch
Protobuf encoded
[]byte{0xb} + block (big endian encoded)
A batch of outgoing transactions
types.OutgoingTxBatch
Protobuf encoded
Monotonically increasing value for each batch created on Injective by some Batch Creator
[]byte{0x7} + []byte("lastBatchId")
Last used batch ID
uint64
Big endian encoded
Represents the latest slashed block height. There is always only a singe value stored.
[]byte{0xf7}
Latest height a batch slashing occurred
uint64
Big endian encoded
Represents the latest bloch height at which a Validator
started unbonding from the Validator Set
. Used to determine slashing conditions.
[]byte{0xf8}
Latest height at which a Validator started unbonding
uint64
Big endian encoded
A denom that is originally from a counter chain will be from a contract. The token contract and denom are stored in two ways. First, the denom is used as the key and the value is the token contract. Second, the contract is used as the key, the value is the denom the token contract represents.
[]byte{0xf3} + []byte(denom)
Token contract address
[]byte
stored in byte format
[]byte{0xf4} + []byte(tokenContract)
Token denom
[]byte
stored in byte format
This entry represents the last observed Valset that was successfully relayed to Ethereum. Updates after an attestation of ValsetUpdatedEvent
has been processed on Injective.
[]byte{0xfa}
Last observed Valset on Ethereum
types.Valset
Protobuf encoded
The nonce of the last observed event on Ethereum. This is set when TryAttestation()
is called. There is always only a single value held in this store.
[]byte{0xf2}
Last observed event nonce
uint64
Big endian encoded
This block height of the last observed event on Ethereum. There will always only be a single value stored in this store.
[]byte{0xf9}
Last observed Ethereum Height
uint64
Protobuf encoded
This is the last observed event on Ethereum from a particular Validator
. Updated every time the asssociated Orchestrator
sends an event claim.
[]byte{0xf1} + []byte(validator address)
Last observed event by some Validator
types.LastClaimEvent
Protobuf encoded
Attestation is an aggregate of claims that eventually becomes observed by all orchestrators as more votes (claims) are coming in. Once observed the claim's particular logic gets executed.
Each attestation is bound to a unique event nonce (generated by Peggy contract
) and they must be processed in order. This is a correctness issue, if relaying out of order transaction replay attacks become possible.
[]byte{0x5} + event nonce (big endian encoded) + []byte(claimHash)
Attestation of occurred events/claims
types.Attestation
Protobuf encoded
A computed hash indicating that a validator set/token batch in fact existed on Injective. This checkpoint also exists in Peggy contract
. Updated on each new valset update and token batch creation.
[]byte{0x1b}
Last created checkpoint hash on Injective
gethcommon.Hash
store in byte format
A list of known malicious Ethereum addresses that are prevented from using the bridge.
[]byte{0x1c} + []byte(ethereum address)
Empty []byte slice
gethcommon.Hash
stored in byte format]
This is a reference document for Peggy message types. For code reference and exact arguments see the proto definitions.
These are messages sent on the Injective Chain peggy module by the end user. See workflow for a more detailed summary of the entire deposit and withdraw process.
Sent to Injective whenever a user wishes to make a withdrawal back to Ethereum. Submitted amount is removed from the user's balance immediately. The withdrawal is added to the outgoing tx pool as a types.OutgoingTransferTx
where it will remain until it is included in a batch.
This message allows the user to cancel a specific withdrawal that is not yet batched. User balance is refunded (Amount
+ BridgeFee
).
This call allows anyone to submit evidence that a validator has signed a valset or batch that never existed. Subject contains the batch or valset.
These messages are sent by the Batch Creator
subprocess of peggo
This message is sent whenever some Batch Creator
finds pooled withdrawals that when batched would satisfy their minimum batch fee (PEGGO_MIN_BATCH_FEE_USD
). After receiving this message the Peggy module
collects all withdrawals of the requested token denom, creates a unique token batch (types.OutgoingTxBatch
) and places it in the Outgoing Batch pool
. Withdrawals that are batched cannot be cancelled with MsgCancelSendToEth
.
These messages are sent by the Oracle
subprocess of peggo
Sent to Injective when a SendToInjectiveEvent
is emitted from the Peggy contract
. This occurs whenever a user is making an individual deposit from Ethereum to Injective.
Sent to Injective when a TransactionBatchExecutedEvent
is emitted from the Peggy contract
. This occurs when a Relayer
has successfully called submitBatch
on the contract to complete a batch of withdrawals.
Sent to Injective when a ValsetUpdatedEvent
is emitted from the Peggy contract
. This occurs when a Relayer
has successfully called updateValset
on the contract to update the Validator Set
on Ethereum.
Sent to Injective when a ERC20DeployedEvent
is emitted from the Peggy contract
. This occurs whenever the deployERC20
method is called on the contract to issue a new token asset eligible for bridging.
These messages are sent by the Signer
subprocess of peggo
When Signer
finds a batch that the Orchestrator
(Validator
) has not signed off, it constructs a signature with its Delegated Ethereum Key
and sends the confirmation to Injective. It's crucial that a Validator
eventually provides their confirmation for a created batch as they will be slashed otherwise.
When Signer
finds a valset update that the Orchestrator
(Validator
) has not signed off, it constructs a signature with its Delegated Ethereum Key
and sends the confirmation to Injective. It's crucial that a Validator
eventually provides their confirmation for a created valset update as they will be slashed otherwise.
The Relayer
does not send any message to Injective, rather it constructs Ethereum transactions with Injective data to update the Peggy contract
via submitBatch
and updateValset
methods.
These are messages sent directly using the validator's message key.
Sent to Injective by an Operator
managing a Validator
node. Before being able to start their Orchestrator
(peggo
) process, they must register a chosen Ethereum address to represent their Validator
on Ethereum. Optionally, an additional Injective address can be provided (Orchestrator
field) to represent that Validator
in the bridging process (peggo
). Defaults to Validator
's own address if omitted.
This message sets the Orchestrator's delegate keys.
The Validator Set is the actual set of keys with stake behind them, which are slashed for double-signs or other misbehavior. We typically consider the security of a chain to be the security of a Validator Set. This varies on each chain, but is our gold standard. Even IBC offers no more security than the minimum of both involved Validator Sets.
The Eth bridge relayer is a binary run alongside the main injectived
daemon by the validator set. It exists purely as a matter of code organization and is in charge of signing Ethereum transactions, as well as observing events on Ethereum and bringing them into the Injective Chain state. It signs transactions bound for Ethereum with an Ethereum key, and signs over events coming from Ethereum with an Injective Chain account key. We can add slashing conditions to any mis-signed message by any Eth Signer run by the Validator Set and be able to provide the same security as the Validator Set, just a different module detecting evidence of malice and deciding how much to slash. If we can prove a transaction signed by any Eth Signer of the Validator Set was illegal or malicious, then we can slash on the Injective Chain side and potentially provide 100% of the security of the Validator Set. Note that this also has access to the 3 week unbonding period to allow evidence to slash even if they immediately unbond.
Below are various slashing conditions we use in Peggy.
This slashing condition is intended to stop validators from signing over a validator set and nonce that has never existed on the Injective Chain. It works via an evidence mechanism, where anyone can submit a message containing the signature of a validator over a fake validator set. This is intended to produce the effect that if a cartel of validators is formed with the intention of submitting a fake validator set, one defector can cause them all to be slashed.
Implementation considerations:
The trickiest part of this slashing condition is determining that a validator set has never existed on Injective. To save space, we will need to clean up old validator sets. We could keep a mapping of validator set hash to true in the KV store, and use that to check if a validator set has ever existed. This is more efficient than storing the whole validator set, but its growth is still unbounded. It might be possible to use other cryptographic methods to cut down on the size of this mapping. It might be OK to prune very old entries from this mapping, but any pruning reduces the deterrence of this slashing condition.
The implemented version of this slashing condition stores a map of hashes for all past events, this is smaller than storing entire batches or validator sets and doesn't have to be accessed as frequently. A possible but not currently implemented efficiency optimization would be to remove hashes from this list after a given period. But this would require storing more metadata about each hash.
Currently automatic evidence submission is not implemented in the relayer. By the time a signature is visible on Ethereum it's already too late for slashing to prevent bridge highjacking or theft of funds. Furthermore since 66% of the validator set is required to perform this action anyways that same controlling majority could simply refuse the evidence. The most common case envisioned for this slashing condition is to break up a cabal of validators trying to take over the bridge by making it more difficult for them to trust one another and actually coordinate such a theft.
The theft would involve exchanging of slashable Ethereum signatures and open up the possibility of a manual submission of this message by any defector in the group.
Currently this is implemented as an ever growing array of hashes in state.
This slashing condition is triggered when a validator does not sign a transaction batch within SignedBatchesWindow
upon it's creation by the Peggy module. This prevents two bad scenarios-
A validator simply does not bother to keep the correct binaries running on their system,
A cartel of >1/3 validators unbond and then refuse to sign updates, preventing any batches from getting enough signatures to be submitted to the Peggy Ethereum contract.
This slashing condition is triggered when a validator does not sign a validator set update which is produced by the Peggy module. This prevents two bad scenarios-
A validator simply does not bother to keep the correct binaries running on their system,
A cartel of >1/3 validators unbond and then refuse to sign updates, preventing any validator set updates from getting enough signatures to be submitted to the Peggy Ethereum contract. If they prevent validator set updates for longer than the Injective Chain unbonding period, they can no longer be punished for submitting fake validator set updates and tx batches (PEGGYSLASH-01 and PEGGYSLASH-03).
To deal with scenario 2, PEGGYSLASH-03 will also need to slash validators who are no longer validating, but are still in the unbonding period for up to UnbondSlashingValsetsWindow
blocks. This means that when a validator leaves the validator set, they will need to keep running their equipment for at least UnbondSlashingValsetsWindow
blocks. This is unusual for the Injective Chain, and may not be accepted by the validators.
The current value of UnbondSlashingValsetsWindow
is 25,000 blocks, or about 12-14 hours. We have determined this to be a safe value based on the following logic. So long as every validator leaving the validator set signs at least one validator set update that they are not contained in then it is guaranteed to be possible for a relayer to produce a chain of validator set updates to transform the current state on the chain into the present state.
It should be noted that both PEGGYSLASH-02 and PEGGYSLASH-03 could be eliminated with no loss of security if it where possible to perform the Ethereum signatures inside the consensus code. This is a pretty limited feature addition to Tendermint that would make Peggy far less prone to slashing.
The Ethereum oracle code (currently mostly contained in attestation.go), is a key part of Peggy. It allows the Peggy module to have knowledge of events that have occurred on Ethereum, such as deposits and executed batches. PEGGYSLASH-03 is intended to punish validators who submit a claim for an event that never happened on Ethereum.
Implementation considerations
The only way we know whether an event has happened on Ethereum is through the Ethereum event oracle itself. So to implement this slashing condition, we slash validators who have submitted claims for a different event at the same nonce as an event that was observed by >2/3s of validators.
Although well-intentioned, this slashing condition is likely not advisable for most applications of Peggy. This is because it ties the functioning of the Injective Chain which it is installed on to the correct functioning of the Ethereum chain. If there is a serious fork of the Ethereum chain, different validators behaving honestly may see different events at the same event nonce and be slashed through no fault of their own. Widespread unfair slashing would be very disruptive to the social structure of the Injective Chain.
Maybe PEGGYSLASH-04 is not necessary at all:
The real utility of this slashing condition is to make it so that, if >2/3 of the validators form a cartel to all submit a fake event at a certain nonce, some number of them can defect from the cartel and submit the real event at that nonce. If there are enough defecting cartel members that the real event becomes observed, then the remaining cartel members will be slashed by this condition. However, this would require >1/2 of the cartel members to defect in most conditions.
If not enough of the cartel defects, then neither event will be observed, and the Ethereum oracle will just halt. This is a much more likely scenario than one in which PEGGYSLASH-04 is actually triggered.
Also, PEGGYSLASH-04 will be triggered against the honest validators in the case of a successful cartel. This could act to make it easier for a forming cartel to threaten validators who do not want to join.
This is similar to PEGGYSLASH-04, but it is triggered against validators who do not submit an oracle claim that has been observed. In contrast to PEGGYSLASH-04, PEGGYSLASH-05 is intended to punish validators who stop participating in the oracle completely.
Implementation considerations
Unfortunately, PEGGYSLASH-05 has the same downsides as PEGGYSLASH-04 in that it ties the correct operation of the Injective Chain to the Ethereum chain. Also, it likely does not incentivize much in the way of correct behavior. To avoid triggering PEGGYSLASH-05, a validator simply needs to copy claims which are close to becoming observed. This copying of claims could be prevented by a commit-reveal scheme, but it would still be easy for a "lazy validator" to simply use a public Ethereum full node or block explorer, with similar effects on security. Therefore, the real usefulness of PEGGYSLASH-05 is likely minimal
PEGGYSLASH-05 also introduces significant risks. Mostly around forks on the Ethereum chain. For example recently OpenEthereum failed to properly handle the Berlin hardfork, the resulting node 'failure' was totally undetectable to automated tools. It didn't crash so there was no restart to perform, blocks where still being produced although extremely slowly. If this had occurred while Peggy was running with PEGGYSLASH-05 active it would have caused those validators to be removed from the set. Possibly resulting in a very chaotic moment for the chain as dozens of validators where removed for little to no fault of their own.
Without PEGGYSLASH-04 and PEGGYSLASH-05, the Ethereum event oracle only continues to function if >2/3 of the validators voluntarily submit correct claims. Although the arguments against PEGGYSLASH-04 and PEGGYSLASH-05 are convincing, we must decide whether we are comfortable with this fact. Alternatively we must be comfortable with the Injective Chain potentially halting entirely due to Ethereum generated factors.
Upon the end of each block the following operations are performed to the state of the module
A validator is slashed for not signing over a valset update which passed the SignedValsetsWindow
. In other words, if a validator fails to provide the confirmation for a valset update within a preconfigured amount of time, they will be slashed for SlashFractionValset
portion of their stake and get jailed immediately.
A validator is slashed for not signing over a batch which passed the SignedBatchesWindow
. In other words, if a validator fails to provide the confirmation for a batch within a preconfigured amount of time, they will be slashed for SlashFractionBatch
portion of their stake and get jailed immediately.
Any batch still present in the Outgoing Batch pool
whose BatchTimeout
(a designated Ethereum height by which the batch should have executed) is exceeded gets removed from the pool and the withdrawals are reinserted back into the Outgoing Tx pool
.
A new Validator Set
update will be created automatically when:
there is a power diff greater than 5% between the latest and current validator set
a validator begins unbonding
The new validator set is eventually relayed to Peggy contract
on Ethereum.
Previously observed valsets that passed the SignedValsetsWindow
are removed from the state
Processes all attestations (an aggregate of claims for a particular event) currently being voted on. Each attestation is processed one by one to ensure each Peggy contract
event is processed. After each processed attestation the module's lastObservedEventNonce
and lastObservedEthereumBlockHeight
are updated.
Depending on the type of claim in the attestation, the following is executed:
MsgDepositClaim
: deposited tokens are minted/unlocked for the receiver address
MsgWithdrawClaim
: corresponding batch is removed from the outgoing pool and any previous batch is cancelled
MsgValsetUpdatedClaim
: the module's LastObservedValset
is updated
MsgERC20DeployedClaim
: new token metadata is validated and registered within the module's state (denom <-> token_contract
)
Previously processed attestations (height earlier that lastObservedEthereumBlockHeight
) are removed from the module state
The peggy module emits the following events:
int32
attestation_type
{attestation_type}
string
bridge_contract
{bridge_contract_address}
uint64
bridge_chain_id
{bridge_chain_id}
[]byte
attestation_id
{attestation_id}
uint64
nonce
{event_nonce}
string
reason
{reason_for_slashing}
int64
power
{validator_power}
string
consensus_address
{consensus_addr}
string
operator_address
{operator_addr}
string
moniker
{validator_moniker}
string
validator_address
{validator_addr}
string
orchestrator_address
{orchestrator_addr}
string
operator_eth_address
{eth_addr}
message
outgoing_tx_id
{tx_id}
string
sender
{sender_addr}
string
receiver
{dest_addr}
sdk.Coin
amount
{token_amount}
sdk.Coin
bridge_fee
{token_amount}
withdrawal_cancelled
bridge_contract
{bridge_contract}
withdrawal_cancelled
bridge_chain_id
{bridge_chain_id}
string
denom
{token_denom}
string
orchestrator_address
{orch_addr}
uint64
batch_nonce
{batch_nonce}
uint64
batch_timeout
{block_height}
[]uint64
batch_tx_ids
{ids}
string
bridge_contract
{bridge_contract}
uint64
bridge_chain_id
{bridge_chain_id}
uint64
batch_id
{id}
uint64
nonce
{nonce}
uint64
valset_nonce
{nonce}
string
orchestrator_address
{prch_addr}
uint64
batch_nonce
{nonce}
string
orchestrator_address
{orch_addr}
uint64
event_nonce
{event_nonce}
uint64
event_height
{event_height}
[]byte
attestation_id
{attestation_key}
string
ethereum_sender
{sender_addr}
string
cosmos_receiver
{receiver_addr}
string
token_contract
{contract_addr}
sdk.Int
amount
{token_amount}
string
orchestrator_address
{orch_addr}
string
data
{custom_data}
uint64
event_nonce
{event_nonce{
uint64
event_height
{event_height}
[]byte
attestation_id
{attestation_key}
uint64
batch_nonce
{batch_nonce}
string
token_contract
{contract_addr}
string
orchestrator_address
{orch_addr}
uint64
event_nonce
{event_nonce}
uint64
event_height
{event_height}
[]byte
attestation_id
{attestation_key}
string
cosmos_denom
{token_denom}
string
token_contract
{token_conntract_addr}
string
name
{token_name}
string
symbol
{token_symbol}
uint64
decimals
{token_decimals}
string
orchestrator_address
{orch_addr}
uint64
event_nonce
{event_nonce}
uint64
event_height
{event_height}
[]byte
attestation_id
{attestation_key}
uint64
valset_nonce
{valset_nonce}
[]*BridgeValidator
valset_members
{array_of_validators}
sdk.Int
reward_amount
{amount}
string
reward_token
{contract_addr}
string
orchestrator_address
{orch_addr}
This document describes and advises configuration of the Peggy module's parameters. The default parameters can be found in the genesis.go of the peggy module.
peggy_id
A random 32 byte value to prevent signature reuse, for example if the Injective Chain validators decided to use the same Ethereum keys for another chain also running Peggy we would not want it to be possible to play a deposit from chain A back on chain B's Peggy. This value IS USED ON ETHEREUM so it must be set in your genesis.json before launch and not changed after deploying Peggy. Changing this value after deploying Peggy will result in the bridge being non-functional. To recover just set it back to the original value the contract was deployed with.
contract_source_hash
The code hash of a known good version of the Peggy contract solidity code. This can be used to verify the correct version of the contract has been deployed. This is a reference value for governance action only it is never read by any Peggy code
bridge_ethereum_address
is address of the bridge contract on the Ethereum side, this is a reference value for governance only and is not actually used by any Peggy module code.
The Ethereum bridge relayer use this value to interact with Peggy contract for querying events and submitting valset/batches to Peggy contract.
bridge_chain_id
The bridge chain ID is the unique identifier of the Ethereum chain. This is a reference value only and is not actually used by any Peggy code
These reference values may be used by future Peggy client implementations to allow for consistency checks.
signed_valsets_window
signed_batches_window
signed_claims_window
These values represent the time in blocks that a validator has to submit a signature for a batch or valset, or to submit a claim for a particular attestation nonce.
In the case of attestations this clock starts when the attestation is created, but only allows for slashing once the event has passed. Note that that claims slashing is not currently enabled see slashing spec
target_batch_timeout
This is the 'target' value for when batches time out, this is a target because Ethereum is a probabilistic chain and you can't say for sure what the block frequency is ahead of time.
average_block_time
average_ethereum_block_time
These values are the average Injective Chain block time and Ethereum block time respectively and they are used to compute what the target batch timeout is. It is important that governance updates these in case of any major, prolonged change in the time it takes to produce a block
slash_fraction_valset
slash_fraction_batch
slash_fraction_claim
slash_fraction_conflicting_claim
The slashing fractions for the various peggy related slashing conditions. The first three refer to not submitting a particular message, the third for failing to submit a claim and the last for submitting a different claim than other validators.
Note that claim slashing is currently disabled as outlined in the slashing spec
valset_reward
Valset reward is the reward amount paid to a relayer when they relay a valset to the Peggy contract on Ethereum.
This document is designed to assist developers in implementing alternate Peggy relayers. The two major components of the Orchestrator which interact with Ethereum. The Peggy bridge has been designed for increased efficiency, not for ease of use. This means there are many implicit requirements of these external binaries which this document does it's best to make explicit.
The Peggy orchestrator
combines three distinct roles that need to be performed by external binaries in the Peggy bridge. This document highlights the requirements of the relayer
which is one of those roles included in the orchestrator
.
When updating the validator set in the Peggy contract you must provide a copy of the old validator set. This MUST only be taken from the last ValsetUpdated event on the Ethereum chain.
Providing the old validator set is part of a storage optimization, instead of storing the entire validator set in Ethereum storage it is instead provided by each caller and stored in the much cheaper Ethereum event queue. No sorting of any kind is performed in the Peggy contract, meaning the list of validators and their new signatures must be submitted in exactly the same order as the last call.
For the purpose of normal operation this requirement can be shortened to 'sort the validators by descending power, and by Eth address bytes where power is equal'. Since the peggy module produces the validator sets they should always come in order. It is not possible for the relayer to change this order since it is part of the signature. But a change in this sorting method on the Peggy module side would halt valset updates and essentially decouple the bridge unless your implementation is smart enough to take a look at the last submitted order rather than blindly following sorting.
The Injective Chain simply produces a stream of validator sets, it does not make any judgement on how they are relayed. It's up to the relayer implementation to determine how to optimize the gas costs of this relaying operation.
For example lets say we had validator sets A, B, C, and D
each is created when there is a 5% power difference between the last Peggy validator set snapshot in the store and the currently active validator set.
5% is an arbitrary constant. The specific value chosen here is a tradeoff made by the chain between how up to date the Ethereum validator set is and the cost to keep it updated. The higher this value is the lower the portion of the voting validator set is needed to highjack the bridge in the worst case. If we made a new validator set update every block 66% would need to collude, the 5% change threshold means 61% of the total voting power colluding in a given validator set may be able to steal the funds in the bridge.
The relayer should iterate over the event history for the Peggy Ethereum contract, it will determine that validator set A is currently in the Peggy bridge. It can choose to either relay validator sets B, C and then D or simply submit validator set D. Provided all validators have signed D it has more than 66% voting power and can pass on it's own. Without paying potentially several hundred dollars more in Ethereum to relay the intermediate sets.
Performing this check locally somehow, before submitting transactions, is essential to a cost effective relayer implementation. You can either use a local Ethereum signing implementation and sum the powers and signatures yourself, or you can simply use the eth_call()
Ethereum RPC to simulate the call on your Ethereum node.
Note that eth_call()
often has funny gotchas. All calls fail on Geth based implementations if you don't have any Ethereum to pay for gas, while on Parity based implementations your gas inputs are mostly ignored and an accurate gas usage is returned.
In order to submit a transaction batch you also need to submit the last set of validators and their staking powers. This is to facilitate the same storage optimization mentioned there.
Making a decision about which batch to relay is very different from deciding which validator set to relay. Batch relaying is primarily motivated by fees, not by a desire to maintain the integrity of the bridge. So the decision mostly comes down to fee computation, this is further complicated by the concept of 'batch requests'. Which is an unpermissioned transaction that requests the Peggy module generate a new batch for a specific token type.
Batch requests are designed to allow the user to withdraw their tokens from the send to Ethereum tx pool at any time up until a relayer shows interest in actually relaying them. While transactions are in the pool there's no risk of a double spend if the user is allowed to withdraw them by sending a MsgCancelSendToEth. Once the transaction enters a batch due to a 'request batch' that is no longer the case and the users funds must remain locked until the Oracle informs the Peggy module that the batch containing the users tokens has become somehow invalid to submit or has been executed on Ethereum.
A relayer uses the query endpoint BatchFees
to iterate over the send to Eth tx pool for each token type, the relayer can then observe the price for the ERC-20 tokens being relayed on a dex and compute the gas cost of executing the batch (via eth_call()
) as well as the gas cost of liquidating the earnings on a dex if desired. Once a relayer determines that a batch is good and profitable it can send a MsgRequestBatch
and the batch will be created for the relayer to relay.
There are also existing batches, which the relayer should also judge for profitability and make an attempt at relaying using much the same method.
Validators run a required Eth Signer
in the peggo orchestrator because we can not yet insert this sort of simple signature logic into Cosmos SDK based chains without significant modification to Tendermint. This may be possible in the future with modifications to Tendermint.
It should be noted that both PEGGYSLASH-02 and PEGGYSLASH-03 could be eliminated with no loss of security if it where possible to perform the Ethereum signatures inside the consensus code. This is a pretty limited feature addition to Tendermint that would make Peggy far less prone to slashing.
Currently validators in Peggy have only one carrot - the extra activity brought to the chain by a functioning bridge.
There are on the other hand a lot of negative incentives (sticks) that the validators must watch out for. These are outlined in the slashing spec.
One negative incentive that is not covered under slashing is the cost of submitting oracle submissions and signatures. Currently these operations are not incentivized, but still cost the validators fees to submit. This isn't a severe issue considering the relatively cheap transaction fees on the Injective Chain currently, but of course is an important factor to consider as transaction fees rise.
Some positive incentives for correctly participating in the operation of the bridge should be under consideration. In addition to eliminating the fees for mandatory submissions.
This document lists the error codes used in the module.
peggy
1
internal
peggy
2
duplicate
peggy
3
invalid
peggy
4
timeout
peggy
5
unknown
peggy
6
empty
peggy
7
outdated
peggy
8
unsupported
peggy
9
non contiguous event nonce
peggy
10
no unbatched txs found
peggy
11
can not set orchestrator addresses more than once
peggy
12
supply cannot exceed max ERC20 value
peggy
13
invalid ethereum sender on claim
peggy
14
invalid ethereum destination
peggy
15
missing previous claim for validator