Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
f364999
Benchmark beefy by signatures
yrong Apr 18, 2025
6d17159
More samples
yrong Apr 18, 2025
aeef3a2
Cleanup
yrong Apr 23, 2025
e3734b3
SubmitFiatShamir
yrong Apr 25, 2025
2d55aa0
Add CreateFiatShamirFinalBitfield
yrong Apr 25, 2025
5bea664
Make requiredSignatures bounded
yrong Apr 28, 2025
f2ed8d2
Beefy relayer
yrong Apr 28, 2025
4f978c0
Cleanup
yrong Apr 28, 2025
bb5c9d2
Merge branch 'ron/benchmark-beefy-by-signatures' into ron/submitFiatS…
yrong Apr 30, 2025
a61d4ca
Generate test fixture
yrong Apr 30, 2025
a34b83d
Merge branch 'main' into ron/benchmark-beefy-by-signatures
yrong Apr 30, 2025
1458b8c
Merge branch 'ron/benchmark-beefy-by-signatures' into ron/submitFiatS…
yrong Apr 30, 2025
e48f5c4
testSubmitFiatShamirWithHandOver
yrong May 2, 2025
afc04c1
Fix compile error
yrong May 2, 2025
ec230b8
Use sha256 as hash function
yrong May 6, 2025
9da095f
Update test fixture
yrong May 6, 2025
e277eb7
Update quorum function
yrong May 6, 2025
b94851e
Switch to sha256d
yrong May 11, 2025
dcc7588
Initialize fiatShamirRequiredSignatures via the constructor
yrong May 11, 2025
b844164
Add an environment variable for local setup
yrong May 12, 2025
98b57d1
Merge branch 'main' into ron/submitFiatShamir
yrong Sep 8, 2025
042e546
Update contract binding
yrong Sep 8, 2025
2aeb930
Submit FiatShamir on demand
yrong Sep 8, 2025
b7ffc9e
Check for outdated commitments in the two-phase commit
yrong Sep 9, 2025
ee36155
Merge branch 'main' into ron/submitFiatShamir
yrong Sep 10, 2025
49cdc61
Add instant syncer
yrong Sep 14, 2025
3e9cb0c
on-demand relayer
yrong Sep 14, 2025
686ae24
Fix command
yrong Sep 15, 2025
ef72c48
Merge branch 'main' into ron/submitFiatShamir
yrong Nov 19, 2025
9b33056
Fix subsample
yrong Nov 19, 2025
dfaca90
Merge branch 'ron/submitFiatShamir' into ron/submitFiatShamir-relay
yrong Nov 19, 2025
0a20d9f
Remove relayer change
yrong Nov 19, 2025
2a44fca
Merge branch 'ron/submitFiatShamir' into ron/submitFiatShamir-relay
yrong Nov 19, 2025
0b48f8b
Revert "Remove relayer change"
yrong Nov 19, 2025
e411810
Polish
yrong Nov 19, 2025
5a696c1
Change interval
yrong Nov 19, 2025
0efa2b3
Merge branch 'main' into ron/submitFiatShamir-relay
yrong Jan 7, 2026
abd5039
Merge branch 'main' into ron/submitFiatShamir-relay
yrong Mar 26, 2026
251ba1f
Revert unrelated
yrong Mar 26, 2026
8e8b6a6
Revert change
yrong Mar 26, 2026
6c72567
Double check
yrong Mar 26, 2026
fd4e3d0
Fee profit estimation
yrong Mar 26, 2026
3f89ae3
Fix
yrong Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions relayer/cmd/run/parachain-v2/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@ import (

"github.com/sirupsen/logrus"
"github.com/snowfork/snowbridge/relayer/chain/ethereum"
"github.com/snowfork/snowbridge/relayer/relays/beefy"
"github.com/snowfork/snowbridge/relayer/relays/parachain-v2"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/sync/errgroup"
)

var (
configFile string
privateKey string
privateKeyFile string
privateKeyID string
configFile string
privateKey string
privateKeyFile string
privateKeyID string
beefyConfigFile string
instantVerification bool
)

func Command() *cobra.Command {
Expand All @@ -38,6 +41,9 @@ func Command() *cobra.Command {
cmd.Flags().StringVar(&privateKeyFile, "ethereum.private-key-file", "", "The file from which to read the private key")
cmd.Flags().StringVar(&privateKeyID, "ethereum.private-key-id", "", "The secret id to lookup the private key in AWS Secrets Manager")

cmd.Flags().StringVar(&beefyConfigFile, "beefy.config", "", "Path to beefy configuration file")
cmd.Flags().BoolVarP(&instantVerification, "instant-verification", "", false, "Enable instant verification of parachain messages")

return cmd
}

Expand Down Expand Up @@ -92,7 +98,26 @@ func run(_ *cobra.Command, _ []string) error {
return nil
})

err = relay.Start(ctx, eg)
if !instantVerification {
err = relay.Start(ctx, eg)
} else {
viper.SetConfigFile(beefyConfigFile)
if err := viper.ReadInConfig(); err != nil {
return err
}

var beefyConfig beefy.Config
err = viper.UnmarshalExact(&beefyConfig)
if err != nil {
return err
}
var instantRelay *parachain.InstantRelay
instantRelay, err = parachain.NewInstantRelay(&config, &beefyConfig, keypair)
if err != nil {
return err
}
err = instantRelay.Start(ctx, eg)
}
if err != nil {
logrus.WithError(err).Fatal("Unhandled error")
cancel()
Expand Down
28 changes: 28 additions & 0 deletions relayer/relays/beefy/ethereum-writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,20 @@ func (wr *EthereumWriter) submit(ctx context.Context, task *Request) error {
}
// Commit PrevRandao which will be used as seed to randomly select subset of validators
// https://github.com/Snowfork/snowbridge/blob/75a475cbf8fc8e13577ad6b773ac452b2bf82fbb/contracts/contracts/BeefyClient.sol#L446-L447
state, err := wr.queryBeefyClientState(ctx)
if err != nil {
return fmt.Errorf("query beefy client state: %w", err)
}

// Ignore beefy block already synced
if uint64(task.SignedCommitment.Commitment.BlockNumber) <= state.LatestBeefyBlock {
log.WithFields(log.Fields{
"validatorSetID": state.CurrentValidatorSetID,
"beefyBlock": state.LatestBeefyBlock,
"relayBlock": task.SignedCommitment.Commitment.BlockNumber,
}).Info("Beefy block already synced, just ignore")
return nil
}
tx, err = wr.contract.CommitPrevRandao(
wr.conn.MakeTxOpts(ctx),
*commitmentHash,
Expand Down Expand Up @@ -210,6 +224,20 @@ func (wr *EthereumWriter) submit(ctx context.Context, task *Request) error {
return nil
}
// Final submission
state, err = wr.queryBeefyClientState(ctx)
if err != nil {
return fmt.Errorf("query beefy client state: %w", err)
}

// Ignore beefy block already synced
if uint64(task.SignedCommitment.Commitment.BlockNumber) <= state.LatestBeefyBlock {
log.WithFields(log.Fields{
"validatorSetID": state.CurrentValidatorSetID,
"beefyBlock": state.LatestBeefyBlock,
"relayBlock": task.SignedCommitment.Commitment.BlockNumber,
}).Info("Beefy block already synced, just ignore")
return nil
}
tx, err = wr.doSubmitFinal(ctx, *commitmentHash, initialBitfield, task)
if err != nil {
if isDuplicateBeefyError(err) {
Expand Down
4 changes: 4 additions & 0 deletions relayer/relays/beefy/on-demand-sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -720,3 +720,7 @@ func (relay *OnDemandRelay) queueAll(ctx context.Context) error {
}
return nil
}

func (relay *OnDemandRelay) GetConfig() *Config {
return relay.config
}
149 changes: 149 additions & 0 deletions relayer/relays/parachain-v2/beefy-instant-syncer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package parachain

import (
"context"
"fmt"
"math/big"
"time"

log "github.com/sirupsen/logrus"
"github.com/snowfork/snowbridge/relayer/relays/beefy"
"golang.org/x/sync/errgroup"
)

type BeefyInstantSyncer struct {
config *Config
beefyListener *BeefyListener
beefyOnDemandRelay *beefy.OnDemandRelay
}

func NewBeefyInstantSyncer(
config *Config,
beefyListener *BeefyListener,
beefyOnDemandRelay *beefy.OnDemandRelay,
) *BeefyInstantSyncer {
return &BeefyInstantSyncer{
config: config,
beefyListener: beefyListener,
beefyOnDemandRelay: beefyOnDemandRelay,
}
}

// Todo: consider using subscription to listen for new finalized beefy headers
func (li *BeefyInstantSyncer) Start(ctx context.Context, eg *errgroup.Group) error {
// Initialize the beefy listener to setup the scanner
err := li.beefyListener.initialize(ctx)
if err != nil {
return fmt.Errorf("initialize beefy listener: %w", err)
}
var fetchInterval time.Duration
if li.config.FetchInterval == 0 {
fetchInterval = 180 * time.Second
} else {
fetchInterval = time.Duration(li.config.FetchInterval) * time.Second
}

ticker := time.NewTicker(fetchInterval)

eg.Go(func() error {

for {
finalizedBeefyBlockHash, err := li.beefyListener.relaychainConn.API().RPC.Beefy.GetFinalizedHead()
if err != nil {
return fmt.Errorf("fetch beefy finalized head: %w", err)
}
finalizedBeefyBlockHeader, err := li.beefyListener.relaychainConn.API().RPC.Chain.GetHeader(finalizedBeefyBlockHash)
if err != nil {
return fmt.Errorf("fetch block header: %w", err)
}
latestBeefyBlockNumber := uint64(finalizedBeefyBlockHeader.Number)
err = li.doScanAndUpdate(ctx, latestBeefyBlockNumber)
if err != nil {
return fmt.Errorf("scan for sync tasks: %w", err)
}

select {
case <-ctx.Done():
return nil
case <-ticker.C:
continue
}
}
})

return nil
}

func (li *BeefyInstantSyncer) isRelayConsensusProfitable(ctx context.Context, tasks []*Task) (bool, error) {
totalFee := new(big.Int)
for _, task := range tasks {
if task == nil || task.MessageProofs == nil || len(*task.MessageProofs) == 0 {
continue
}
for _, messageProof := range *task.MessageProofs {
totalFee.Add(totalFee, &messageProof.Message.Fee)
}
}
gasPrice, err := li.beefyListener.ethereumConn.Client().SuggestGasPrice(ctx)
if err != nil {
return false, fmt.Errorf("suggest gas price: %w", err)
}
var requireFee *big.Int
if li.beefyOnDemandRelay.GetConfig().Sink.EnableFiatShamir {
requireFee = new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(li.config.Sink.Fees.BaseBeefyFiatShamirGas))
} else {
requireFee = new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(li.config.Sink.Fees.BaseBeefyTwoPhaseCommitGas))
}
isProfitable := totalFee.Cmp(requireFee) >= 0
log.WithFields(log.Fields{
"totalFee": totalFee.String(),
"requireFee": requireFee.String(),
"isProfitable": isProfitable,
}).Info("isProfitable")
return isProfitable, nil
}

func (li *BeefyInstantSyncer) doScanAndUpdate(ctx context.Context, beefyBlockNumber uint64) error {
// Scan for undelivered orders using the latest BEEFY block number on the relay chain.
tasks, err := li.beefyListener.scanner.Scan(ctx, beefyBlockNumber)
if err != nil {
return fmt.Errorf("scan for sync tasks: %w", err)
}
if len(tasks) == 0 {
log.Info("No tasks found, skipping")
return nil
}
// Check if the relay consensus is profitable
isProfitable, err := li.isRelayConsensusProfitable(ctx, tasks)
if err != nil {
return fmt.Errorf("check is relay consensus profitable: %w", err)
}
if !isProfitable {
log.Info("Relay consensus is not profitable, skipping")
return nil
}
// Oneshot sync with FiatShamir and ensure light client is synced to the BEEFY block number
// before submitting any messages to the parachain
// This is to ensure the light client has the necessary BEEFY proofs
// to verify the parachain headers being submitted
log.Info(fmt.Sprintf("Syncing light client to BEEFY block number %d\n", beefyBlockNumber))
err = li.beefyOnDemandRelay.OneShotStart(ctx, beefyBlockNumber)
if err != nil {
return fmt.Errorf("sync beefy update on demand: %w", err)
}
beefyBlockSynced, _, err := li.beefyListener.fetchLatestBeefyBlock(ctx)
if err != nil {
return fmt.Errorf("fetch latest beefy block: %w", err)
}
if beefyBlockSynced < beefyBlockNumber {
return fmt.Errorf("beefy block %d not synced to light client, recent synced %d", beefyBlockNumber, beefyBlockSynced)
}
for _, task := range tasks {
err = li.beefyListener.sendTask(ctx, task)
if err != nil {
return fmt.Errorf("send task: %w", err)
}
}

return nil
}
35 changes: 35 additions & 0 deletions relayer/relays/parachain-v2/beefy-listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,38 @@ func (li *BeefyListener) sendTask(ctx context.Context, task *Task) error {
}
return nil
}

func (li *BeefyListener) initialize(ctx context.Context) error {
// Set up light client bridge contract
address := common.HexToAddress(li.config.Contracts.BeefyClient)
beefyClientContract, err := contracts.NewBeefyClient(address, li.ethereumConn.Client())
if err != nil {
return err
}
li.beefyClientContract = beefyClientContract

// fetch ParaId
paraIDKey, err := types.CreateStorageKey(li.parachainConnection.Metadata(), "ParachainInfo", "ParachainId", nil, nil)
if err != nil {
return err
}
var paraID uint32
ok, err := li.parachainConnection.API().RPC.State.GetStorageLatest(paraIDKey, &paraID)
if err != nil {
return fmt.Errorf("fetch parachain id: %w", err)
}
if !ok {
return fmt.Errorf("parachain id missing")
}
li.paraID = paraID

li.scanner = &Scanner{
config: li.config,
ethConn: li.ethereumConn,
relayConn: li.relaychainConn,
paraConn: li.parachainConnection,
paraID: paraID,
ofac: li.ofac,
}
return nil
}
5 changes: 5 additions & 0 deletions relayer/relays/parachain-v2/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Config struct {
Sink SinkConfig `mapstructure:"sink"`
RewardAddress string `mapstructure:"reward-address"`
OFAC config.OFACConfig `mapstructure:"ofac"`
FetchInterval uint32 `mapstructure:"fetch-interval"`
}

type SourceConfig struct {
Expand All @@ -37,6 +38,10 @@ type SinkContractsConfig struct {
}

type FeeConfig struct {
// The gas cost of two phase commit
BaseBeefyTwoPhaseCommitGas uint64 `mapstructure:"base-beefy-two-phase-commit-gas"`
// The gas cost of fiat shamir commit
BaseBeefyFiatShamirGas uint64 `mapstructure:"base-beefy-fiat-shamir-gas"`
// The gas cost of v2_submit excludes command execution, mainly covers the verification
BaseDeliveryGas uint64 `mapstructure:"base-delivery-gas"`
// The gas cost of unlock ERC20 token
Expand Down
Loading
Loading