Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
209 changes: 209 additions & 0 deletions client/cmd/crossMint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package cmd

import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/cloudflare/circl/sign/ed448"
libP2pCrypto "github.com/libp2p/go-libp2p/core/crypto"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"go.uber.org/zap"
"source.quilibrium.com/quilibrium/monorepo/node/config"
"source.quilibrium.com/quilibrium/monorepo/node/keys"
)

var crossMintCmd = &cobra.Command{
Use: "cross-mint",
Short: "Signs a payload from the Quilibrium bridge to mint tokens on Ethereum L1 and prints the result to stdout",
Long: `Signs a payload from the Quilibrium bridge to mint tokens on Ethereum L1 and prints the result to stdout":

cross-mint <Payload>

Payload – the hex-encoded payload from the Quilibrium bridge with optional 0x-prefix, must be specified
`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
fmt.Printf("missing payload")
os.Exit(1)
}

_, err := os.Stat(configDirectory)
if os.IsNotExist(err) {
fmt.Printf("config directory doesn't exist: %s", configDirectory)
os.Exit(1)
}

config, err := config.LoadConfig(configDirectory, "")
if err != nil {
fmt.Printf("invalid config directory: %s", configDirectory)
os.Exit(1)
}

rawPeerKey, err := hex.DecodeString(config.P2P.PeerPrivKey)
if err != nil {
panic(errors.Wrap(err, "cross mint"))
}

peerKey, err := libP2pCrypto.UnmarshalEd448PrivateKey(rawPeerKey)
if err != nil {
panic(errors.Wrap(err, "cross mint"))
}

rawPeerKey, err = peerKey.Raw()
if err != nil {
panic(errors.Wrap(err, "cross mint"))
}

// TODO: support other key managers
// Get the proving key
// `config.Key.KeyStoreFile.Path` defaults to `.config/keys.yml`.
// We do our best here to make sure the configuration value is taken into
// account if it was changed.
if !filepath.IsAbs(config.Key.KeyStoreFile.Path) {
config.Key.KeyStoreFile.Path = filepath.Join(
configDirectory,
filepath.Base(config.Key.KeyStoreFile.Path),
)
}

logger, err := zap.NewProduction()
if err != nil {
panic(errors.Wrap(err, "cross mint"))
}

fileKeyManager := keys.NewFileKeyManager(config.Key, logger)
provingKey, err := fileKeyManager.GetSigningKey(config.Engine.ProvingKeyId)
if err != nil {
panic(errors.Wrap(err, "cross mint"))
}
// Sign the payload
result, err := CrossMint(&CrossMintArgs{
Payload: args[0],
PeerKey: rawPeerKey,
ProvingKey: provingKey.(ed448.PrivateKey),
})
if err != nil {
panic(errors.Wrap(err, "error cross minting"))
}
// Print the result
jsonResult, err := json.Marshal(result)
if err != nil {
panic(errors.Wrap(err, "error marshaling result to json"))
}
fmt.Println(string(jsonResult))
},
}

func init() {
rootCmd.AddCommand(crossMintCmd)
}

// CrossMintArgs Arguments for the cross mint operation
type CrossMintArgs struct {
// Hex encoded payload with optional 0x prefix
Payload string
// The node's ed448 peer key
PeerKey ed448.PrivateKey
// The node's ed448 proving key
ProvingKey ed448.PrivateKey
}

// CrossMintResult Result of the cross mint operation
type CrossMintResult struct {
// Base64 encoded peer public key
PeerPublicKey string `json:"peerPublicKey"`
// Base64 encoded signature of the payload with the peer private key
PeerSignature string `json:"peerSignature"`
// Base64 encoded prover public key
ProverPublicKey string `json:"proverPublicKey"`
// Base64 encoded signature of the payload with the prover private key
ProverSignature string `json:"proverSignature"`
}

func CrossMint(args *CrossMintArgs) (*CrossMintResult, error) {
rawPayload, err := decodeHexString(args.Payload)
if err != nil {
return nil, errors.Wrap(err, "cross mint")
}

peerSignature := ed448.Sign(args.PeerKey, rawPayload, "")
peerPubKey, ok := args.PeerKey.Public().(ed448.PublicKey)
if !ok {
return nil, errors.Wrap(
errors.New("error casting peer public key to ed448 public key"),
"cross mint",
)
}

provingSignature := ed448.Sign(
args.ProvingKey,
rawPayload,
"",
)
provingPubKey, ok := args.ProvingKey.Public().(ed448.PublicKey)
if !ok {
return nil, errors.Wrap(
errors.New("error casting proving public key to ed448 public key"),
"cross mint",
)
}
return &CrossMintResult{
PeerPublicKey: base64.StdEncoding.EncodeToString(peerPubKey),
PeerSignature: base64.StdEncoding.EncodeToString(peerSignature),
ProverPublicKey: base64.StdEncoding.EncodeToString(provingPubKey),
ProverSignature: base64.StdEncoding.EncodeToString(provingSignature),
}, nil
}

// VerifyCrossMint Verify a cross-mint message. Returns true if both signatures
// verify with the given public keys.
func VerifyCrossMint(payload string, result *CrossMintResult) (bool, error) {
payloadBytes, err := decodeHexString(payload)
if err != nil {
return false, err
}
peerPubKeyBytes, err := base64.StdEncoding.DecodeString(result.PeerPublicKey)
if err != nil {
return false, err
}
peerPubKey := ed448.PublicKey(peerPubKeyBytes)
peerSignature, err := base64.StdEncoding.DecodeString(result.PeerSignature)
if err != nil {
return false, err
}
proverPubKeyBytes, err := base64.StdEncoding.DecodeString(
result.ProverPublicKey,
)
if err != nil {
return false, err
}
proverPubKey := ed448.PublicKey(proverPubKeyBytes)
proverSignature, err := base64.StdEncoding.DecodeString(
result.ProverSignature,
)
if err != nil {
return false, err
}
peerSigOk := ed448.Verify(peerPubKey, payloadBytes, peerSignature, "")
proverSigOk := ed448.Verify(proverPubKey, payloadBytes, proverSignature, "")
return peerSigOk && proverSigOk, nil
}

func decodeHexString(hexStr string) ([]byte, error) {
// Check if the string starts with '0x' and remove it
if strings.HasPrefix(hexStr, "0x") {
hexStr = hexStr[2:]
}
// Decode the hex string into bytes
data, err := hex.DecodeString(hexStr)
if err != nil {
return nil, errors.Wrap(err, "error decoding hex string")
}
return data, nil
}
102 changes: 102 additions & 0 deletions client/cmd/crossMint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package cmd_test

import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
"source.quilibrium.com/quilibrium/monorepo/client/cmd"
)

var peerPrivKeyStr = "b14b843edf61a58870d3f96fe7dc8c6d0479af10aa16c45b102ddae75b278e8ef302edca36c71c1b4beb60e88088826c4da71ab0b5bf60cc92fc35078fa499f8a4accd51776ecc1fb520fdb20408133366077dadf4ff8a337fb42a19ff846e3609516a3f61c92ea4b9c2298a64b8bfae3780"
var provingPrivKeyStr = "690a5f08a2b3a0b787a8301c5fded376f7a070f5d89f66bf537d67fa1ed71a2b22ce166733b7d73ccf10a20ef0f04f163aae13883ff8270f93fed723c513e24e7f93d317b3257731887411bf820e169a0180c0cbf625efa67ff54cc22f1d97a2bd3f721705eeb4c2bf022f793954a798f700"

func TestCrossMintRoundtrip(t *testing.T) {
peerPrivKey, err := hex.DecodeString(peerPrivKeyStr)
if err != nil {
t.Fatal(err)
}
provingPrivKey, err := hex.DecodeString(provingPrivKeyStr)
if err != nil {
t.Fatal(err)
}
payloadBytes := make([]byte, 32)
n, err := rand.Read(payloadBytes)
if err != nil {
t.Fatal(err)
}
if n != 32 {
t.Fatal("not enough bytes read")
}
payload := hex.EncodeToString(payloadBytes)
args := &cmd.CrossMintArgs{
Payload: payload,
PeerKey: peerPrivKey,
ProvingKey: provingPrivKey,
}
result, err := cmd.CrossMint(args)
if err != nil {
t.Fatal(err)
}
verified, err := cmd.VerifyCrossMint(payload, result)
assert.True(t, verified, "cross mint verification failed")
// Switch the public keys to induce failure
tmp := result.PeerPublicKey
result.PeerPublicKey = result.ProverPublicKey
result.ProverPublicKey = tmp
verified, err = cmd.VerifyCrossMint(payload, result)
assert.False(t, verified, "cross mint verification should have failed")
}

func TestCrossMint0xHex(t *testing.T) {
peerPrivKey, err := hex.DecodeString(peerPrivKeyStr)
if err != nil {
t.Fatal(err)
}
provingPrivKey, err := hex.DecodeString(provingPrivKeyStr)
if err != nil {
t.Fatal(err)
}

payload := "0x1234"
args := &cmd.CrossMintArgs{
Payload: payload,
PeerKey: peerPrivKey,
ProvingKey: provingPrivKey,
}
result, err := cmd.CrossMint(args)
if err != nil {
t.Fatal(err)
}
verified, err := cmd.VerifyCrossMint(payload, result)
assert.True(t, verified, "cross mint verification failed")
}

func TestCrossMintFixture(t *testing.T) {
// Generated by running the `cross-mint` on the config of a newly started node with payload "1234"
rawJson := `{
"peerPublicKey": "LLP/9tYlqIsV8AgTkyIpK9zjN+OfXNmYMDhmDeEagCMAhjfpPPWDyWDq9w6uMl9hGyDKYB10EV0A",
"peerSignature": "2ybumA9Vu5rnr5nYPcjehGo/PK6uNl4iVa0WXkEGms5ChqPFgOJX6Z5eng8U6VSHy85zbeZBukiANE3j2EBxrk4TAf4Z+5uuNMCQ6DasKpkgsxuIOGWKhOcBal2CDicinuMqafU3YOrXH9cck/OkjywA",
"proverPublicKey": "CAk3inpisW2Bpcar/z5/3dwjRSgFMRbhYhtCWdQThZZqDvOWFGgoMXLyKHw3B4+yEsmYIVaQZ/iA",
"proverSignature": "NZb7D2xNbCLlYGMUN1BiwBJL9Z6LbdRiMT1FpltBVpRNuoUNmncN++TWgJD6ngIv+VxEnWB0fhcAfAvrIlwNL0z8Ppur8mGMwedzJIm3pB22nAjUTDvNOvvczZWNbPZPyXcWco8SIC/aVaNmvsg//SgA"
}`
// load RawJson into a CrossMintResult
result := &cmd.CrossMintResult{}
err := json.Unmarshal([]byte(rawJson), result)
if err != nil {
t.Fatal(err)
}
ok, err := cmd.VerifyCrossMint("1234", result)
if err != nil {
t.Fatal(err)
}
assert.True(t, ok, "cross mint verification failed")

bad, err := cmd.VerifyCrossMint("2234", result)
if err != nil {
t.Fatal(err)
}
assert.False(t, bad, "cross mint verification succeeded when it should have failed")
}
36 changes: 34 additions & 2 deletions client/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,41 @@ module source.quilibrium.com/quilibrium/monorepo/client

go 1.20

replace github.com/libp2p/go-libp2p => ../go-libp2p

replace source.quilibrium.com/quilibrium/monorepo/node => ../node

replace source.quilibrium.com/quilibrium/monorepo/nekryptology => ../nekryptology

require github.com/stretchr/testify v1.8.4

require (
filippo.io/edwards25519 v1.0.0-rc.1 // indirect
github.com/btcsuite/btcd v0.21.0-beta.0.20201114000516-e9c7a5ac6401 // indirect
github.com/bwesterb/go-ristretto v1.2.3 // indirect
github.com/consensys/gnark-crypto v0.5.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/sys v0.17.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
source.quilibrium.com/quilibrium/monorepo/nekryptology v0.0.0-00010101000000-000000000000 // indirect
)

require (
github.com/cloudflare/circl v1.3.8
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/libp2p/go-libp2p v0.33.2
github.com/pkg/errors v0.9.1
github.com/shopspring/decimal v1.4.0
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5 // indirect
go.uber.org/zap v1.27.0
google.golang.org/protobuf v1.32.0 // indirect
source.quilibrium.com/quilibrium/monorepo/node v1.14.17
)
Loading