Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for signature malleability #1335

Merged
merged 14 commits into from
Dec 3, 2024
4 changes: 2 additions & 2 deletions go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e h1:bBLctRc7kr01YGvaDfgLbTwjFNW5jdp5y5rj8XXBHfY=
github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e/go.mod h1:AzA8Lj6YtixmJWL+wkKoBGsLWy9gFrAzi4g+5bCKwpY=
github.com/fjl/memsize v0.0.2/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0=
github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw=
github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8=
github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 h1:IZqZOB2fydHte3kUgxrzK5E1fW7RQGeDwE8F/ZZnUYc=
Expand Down Expand Up @@ -173,7 +172,8 @@ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+l
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc=
github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs=
github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hydrogen18/memlistener v1.0.0 h1:JR7eDj8HD6eXrc5fWLbSUnfcQFL06PYvCc0DKQnWfaU=
github.com/hydrogen18/memlistener v1.0.0/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE=
Expand Down
2 changes: 1 addition & 1 deletion relayer/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ var rootCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(run.Command())
rootCmd.AddCommand(getBlockCmd())
//rootCmd.AddCommand(fetchMessagesCmd())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented code?

rootCmd.AddCommand(subBeefyCmd())
rootCmd.AddCommand(scanBeefyCmd())
rootCmd.AddCommand(scanSingleBeefyBlockCmd())
rootCmd.AddCommand(leafCmd())
rootCmd.AddCommand(basicChannelLeafProofCmd())
rootCmd.AddCommand(parachainHeadProofCmd())
Expand Down
109 changes: 97 additions & 12 deletions relayer/cmd/scan_beefy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ package cmd

import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"

"github.com/sirupsen/logrus"
"github.com/snowfork/go-substrate-rpc-client/v4/types"
"github.com/snowfork/snowbridge/relayer/chain/relaychain"
"github.com/snowfork/snowbridge/relayer/relays/beefy"
"github.com/snowfork/snowbridge/relayer/relays/util"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)
Expand All @@ -25,9 +28,6 @@ func scanBeefyCmd() *cobra.Command {
cmd.Flags().StringP("polkadot-url", "p", "ws://127.0.0.1:9944", "Polkadot URL.")
cmd.Flags().Uint64P("beefy-block", "b", 0, "Beefy block.")
cmd.MarkFlagRequired("beefy-block")
cmd.Flags().Uint64P("validator-set-id", "v", 0, "Validator set id.")
cmd.MarkFlagRequired("validator-set-id")
cmd.Flags().Uint64P("fast-forward-depth", "f", 10000, "Fast forward depth.")
return cmd
}

Expand All @@ -43,24 +43,19 @@ func ScanBeefyFn(cmd *cobra.Command, _ []string) error {
relaychainConn := relaychain.NewConnection(polkadotUrl)
relaychainConn.Connect(ctx)

fastForwardDepth, _ := cmd.Flags().GetUint64("fast-forward-depth")
config := beefy.SourceConfig{
FastForwardDepth: fastForwardDepth,
}
config := beefy.SourceConfig{}
polkadotListener := beefy.NewPolkadotListener(
&config,
relaychainConn,
)

beefyBlock, _ := cmd.Flags().GetUint64("beefy-block")
validatorSetID, _ := cmd.Flags().GetUint64("validator-set-id")
logrus.WithFields(logrus.Fields{
"polkadot-url": polkadotUrl,
"beefy-block": beefyBlock,
"validator-set-id": validatorSetID,
"polkadot-url": polkadotUrl,
"beefy-block": beefyBlock,
}).Info("Connected to relaychain.")

commitments, err := polkadotListener.Start(ctx, eg, beefyBlock, validatorSetID)
commitments, err := polkadotListener.Start(ctx, eg, beefyBlock)
if err != nil {
logrus.WithError(err).Fatalf("could not start")
}
Expand Down Expand Up @@ -103,3 +98,93 @@ func ScanBeefyFn(cmd *cobra.Command, _ []string) error {

return nil
}

func scanSingleBeefyBlockCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "scan-single-beefy-block",
Short: "Scan a single block which contains beefy commitment",
Args: cobra.ExactArgs(0),
RunE: ScanSingleBeefyBlockFn,
}

cmd.Flags().StringP("polkadot-url", "p", "ws://127.0.0.1:9944", "Polkadot URL.")
cmd.Flags().Uint64P("beefy-block", "b", 0, "Beefy block.")
cmd.MarkFlagRequired("beefy-block")
return cmd
}

func ScanSingleBeefyBlockFn(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
log.SetOutput(logrus.WithFields(logrus.Fields{"logger": "stdlib"}).WriterLevel(logrus.InfoLevel))
logrus.SetLevel(logrus.DebugLevel)

polkadotUrl, _ := cmd.Flags().GetString("polkadot-url")
relaychainConn := relaychain.NewConnection(polkadotUrl)
err := relaychainConn.Connect(ctx)
if err != nil {
fmt.Errorf("connect: %w", err)
return err
}
api := relaychainConn.API()
// metadata := relaychainConn.Metadata()

beefyBlockNumber, _ := cmd.Flags().GetUint64("beefy-block")
logrus.WithFields(logrus.Fields{
"polkadot-url": polkadotUrl,
"beefy-block": beefyBlockNumber,
}).Info("Connected to relaychain.")

beefyBlockHash, err := api.RPC.Chain.GetBlockHash(beefyBlockNumber)
if err != nil {
return fmt.Errorf("fetch hash: %w", err)
}

beefyBlock, err := api.RPC.Chain.GetBlock(beefyBlockHash)
if err != nil {
return fmt.Errorf("fetch block: %w", err)
}

var commitment *types.SignedCommitment
for j := range beefyBlock.Justifications {
sc := types.OptionalSignedCommitment{}
if beefyBlock.Justifications[j].EngineID() == "BEEF" {
err := types.DecodeFromBytes(beefyBlock.Justifications[j].Payload(), &sc)
if err != nil {
return fmt.Errorf("decode BEEFY signed commitment: %w", err)
}
ok, value := sc.Unwrap()
if ok {
commitment = &value
}
}
}
if commitment == nil {
return fmt.Errorf("beefy block without a valid commitment")
}
if len(commitment.Signatures) == 0 {
return fmt.Errorf("no signature in the commitment")
}
var emptyNum uint
var errNum uint
var revertedNum uint
for _, s := range commitment.Signatures {
ok, beefySig := s.Unwrap()
if !ok {
emptyNum++
continue
}
sBefore := util.BytesToHexString(beefySig[32:64])
_, _, s, reverted, err := beefy.CleanSignature(beefySig)
if err != nil {
logrus.WithError(err).Warn("cleanSignature")
errNum++
}
sAfter := util.BytesToHexString(s[:])
if reverted {
revertedNum++
logrus.Info(fmt.Sprintf("s is reverted, before clean:%s, after clean:%s", sBefore, sAfter))
}
}
logrus.Info(fmt.Sprintf("number of total signatures:%d,empty signatures:%d,invalid signatures:%d,reverted signatures:%d", len(commitment.Signatures), emptyNum, errNum, revertedNum))
return nil
}
1 change: 1 addition & 0 deletions relayer/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
github.com/stretchr/testify v1.8.4
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e
golang.org/x/sync v0.6.0
github.com/holiman/uint256 v1.3.1
)

require (
Expand Down
5 changes: 1 addition & 4 deletions relayer/relays/beefy/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package beefy

import (
"fmt"

"github.com/snowfork/snowbridge/relayer/config"
)

Expand All @@ -12,10 +13,6 @@ type Config struct {

type SourceConfig struct {
Polkadot config.PolkadotConfig `mapstructure:"polkadot"`
// Depth to ignore the beefy updates too far away (in number of blocks)
FastForwardDepth uint64 `mapstructure:"fast-forward-depth"`
// Period to sample the beefy updates (in number of blocks)
UpdatePeriod uint64 `mapstructure:"update-period"`
}

type SinkConfig struct {
Expand Down
2 changes: 1 addition & 1 deletion relayer/relays/beefy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func (relay *Relay) Start(ctx context.Context, eg *errgroup.Group) error {
"validatorSetID": initialState.CurrentValidatorSetID,
}).Info("Retrieved current BeefyClient state")

requests, err := relay.polkadotListener.Start(ctx, eg, initialState.LatestBeefyBlock, initialState.CurrentValidatorSetID)
requests, err := relay.polkadotListener.Start(ctx, eg, initialState.LatestBeefyBlock)
if err != nil {
return fmt.Errorf("initialize polkadot listener: %w", err)
}
Expand Down
51 changes: 44 additions & 7 deletions relayer/relays/beefy/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"math/big"

"github.com/holiman/uint256"
"github.com/sirupsen/logrus"
"github.com/snowfork/go-substrate-rpc-client/v4/types"
"github.com/snowfork/snowbridge/relayer/contracts"
Expand Down Expand Up @@ -63,7 +64,10 @@ func (r *Request) MakeSubmitInitialParams(valAddrIndex int64, initialBitfield []
return nil, fmt.Errorf("convert to ethereum address: %w", err)
}

v, _r, s := cleanSignature(validatorSignature)
v, _r, s, _, err := CleanSignature(validatorSignature)
if err != nil {
return nil, fmt.Errorf("clean signature: %w", err)
}

msg := InitialRequestParams{
Commitment: *commitment,
Expand All @@ -89,13 +93,43 @@ func toBeefyClientCommitment(c *types.Commitment) *contracts.BeefyClientCommitme
}
}

func cleanSignature(input types.BeefySignature) (uint8, [32]byte, [32]byte) {
// Implementation derived from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/5bb3f3e788c6b2c806d562ef083b438354f969d7/contracts/utils/cryptography/ECDSA.sol#L139-L145
func CleanSignature(input types.BeefySignature) (v uint8, r [32]byte, s [32]byte, reverted bool, err error) {
yrong marked this conversation as resolved.
Show resolved Hide resolved
// Update signature format (Polkadot uses recovery IDs 0 or 1, Eth uses 27 or 28, so we need to add 27)
// Split signature into r, s, v and add 27 to v
r := *(*[32]byte)(input[:32])
s := *(*[32]byte)(input[32:64])
v := byte(uint8(input[64]) + 27)
return v, r, s
r = *(*[32]byte)(input[:32])
s = *(*[32]byte)(input[32:64])
v = uint8(input[64])
if v < 27 {
v += 27
}
if v != 27 && v != 28 {
return v, r, s, reverted, fmt.Errorf("invalid V:%d", v)
}
var N *uint256.Int = uint256.NewInt(0)
N.SetFromHex("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141")
yrong marked this conversation as resolved.
Show resolved Hide resolved
var halfN *uint256.Int = uint256.NewInt(0)
halfN.SetFromHex("0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0")
var s256 *uint256.Int = uint256.NewInt(0)
err = s256.SetFromHex(util.BytesToHexString(s[:]))
if err != nil && err != uint256.ErrLeadingZero {
return v, r, s, reverted, fmt.Errorf("invalid S:%s,error is:%w", util.BytesToHexString(s[:]), err)
}
// If polkadot library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
// with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
// vice versa.
if s256.Gt(halfN) {
var negativeS256 *uint256.Int = uint256.NewInt(0)
negativeS256 = negativeS256.Sub(N, s256)
s = negativeS256.Bytes32()
if v%2 == 0 {
v = v - 1
} else {
v = v + 1
}
reverted = true
}
return v, r, s, reverted, nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reverted variable doesn't seem to be used anywhere?

Copy link
Contributor Author

@yrong yrong Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if reverted {
revertedNum++
logrus.Info(fmt.Sprintf("s is reverted, before clean:%s, after clean:%s", sBefore, sAfter))
}

Just used to inspect the beefy signatures.

}

func (r *Request) generateValidatorAddressProof(validatorIndex int64) ([][32]byte, error) {
Expand Down Expand Up @@ -132,7 +166,10 @@ func (r *Request) MakeSubmitFinalParams(validatorIndices []uint64, initialBitfie
return nil, fmt.Errorf("signature is empty")
}

v, _r, s := cleanSignature(beefySig)
v, _r, s, _, err := CleanSignature(beefySig)
if err != nil {
return nil, fmt.Errorf("clean signature: %w", err)
}
account, err := r.Validators[validatorIndex].IntoEthereumAddress()
if err != nil {
return nil, fmt.Errorf("convert to ethereum address: %w", err)
Expand Down
51 changes: 51 additions & 0 deletions relayer/relays/beefy/parameters_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package beefy

import (
"testing"

"github.com/snowfork/go-substrate-rpc-client/v4/types"
"github.com/snowfork/snowbridge/relayer/relays/util"
"github.com/stretchr/testify/assert"
)

func TestCleanSignatureNochange(t *testing.T) {
r, err := util.HexStringTo32Bytes("0xc1d9e2b5dd63860d27c38a8b276e5a5ab5e19a97452b0cb24094613bcbd517d8")
s, err := util.HexStringTo32Bytes("0x6dc0d1a7743c3328bfcfe05a2f8691e114f9143776a461ddad6e8b858bb19c1d")
v := byte(28)
signature := buildSignature(v, r, s)
vAfter, rAfter, sAfter, reverted, err := CleanSignature(signature)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, reverted, false)
assert.Equal(t, vAfter, v)
assert.Equal(t, rAfter, r)
assert.Equal(t, sAfter, s)
}

func TestCleanSignatureWithSConverted(t *testing.T) {
r, err := util.HexStringTo32Bytes("0xc1d9e2b5dd63860d27c38a8b276e5a5ab5e19a97452b0cb24094613bcbd517d8")
s, err := util.HexStringTo32Bytes("0x923f2e588bc3ccd740301fa5d0796e1da5b5c8af38a43e5e1263d3074484a524")
v := byte(27)
signature := buildSignature(v, r, s)

negativeS, err := util.HexStringTo32Bytes("0x6dc0d1a7743c3328bfcfe05a2f8691e114f9143776a461ddad6e8b858bb19c1d")
negativeV := byte(28)

vAfter, rAfter, sAfter, reverted, err := CleanSignature(signature)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, reverted, true)
assert.Equal(t, vAfter, negativeV)
assert.Equal(t, rAfter, r)
assert.Equal(t, sAfter, negativeS)
}

func buildSignature(v uint8, r [32]byte, s [32]byte) (signature types.BeefySignature) {
var input []byte
input = append(r[:], s[:]...)
input = append(input, v)
copy(signature[:], input)
return signature
}
5 changes: 1 addition & 4 deletions relayer/relays/beefy/polkadot-listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,12 @@ func (li *PolkadotListener) Start(
ctx context.Context,
eg *errgroup.Group,
currentBeefyBlock uint64,
currentValidatorSetID uint64,
) (<-chan Request, error) {
requests := make(chan Request, 1)

eg.Go(func() error {
defer close(requests)
err := li.scanCommitments(ctx, currentBeefyBlock, currentValidatorSetID, requests)
err := li.scanCommitments(ctx, currentBeefyBlock, requests)
if err != nil {
return err
}
Expand All @@ -51,7 +50,6 @@ func (li *PolkadotListener) Start(
func (li *PolkadotListener) scanCommitments(
ctx context.Context,
currentBeefyBlock uint64,
currentValidatorSet uint64,
requests chan<- Request,
) error {
in, err := ScanCommitments(ctx, li.conn.Metadata(), li.conn.API(), currentBeefyBlock+1)
Expand Down Expand Up @@ -92,7 +90,6 @@ func (li *PolkadotListener) scanCommitments(
"validatorSetID": validatorSetID,
"nextValidatorSetID": nextValidatorSetID,
},
"validatorSetID": currentValidatorSet,
}).Info("Sending BEEFY commitment to ethereum writer")

select {
Expand Down
Loading