protocol update: update contract to verify hash of private spend key, saving on gas (#37)

This commit is contained in:
noot
2021-11-22 00:01:53 -05:00
committed by GitHub
parent 5da175b471
commit e32fc7ba48
16 changed files with 350 additions and 851 deletions

View File

@@ -4,48 +4,7 @@ This is a WIP prototype of ETH<->XMR atomic swaps, currently in the early develo
## Protocol
Alice has ETH and wants XMR, Bob has XMR and wants ETH. They come to an agreement to do the swap and the amounts they will swap.
#### Initial (offchain) phase
- Alice and Bob each generate Monero secret keys (which consist of secret spend and view keys): (`s_a`, `v_a`) and (`s_b`, `v_b`), which are used to construct valid points on the ed25519 curve (ie. public keys): `P_a` and `P_b` accordingly. Alice sends Bob her public key and Bob sends Alice his public spend key and private view key. Note: The XMR will be locked in the account with address corresponding to the public key `P_a + P_b`. Bob needs to send his private view key so Alice can check that Bob actually locked the amount of XMR he claims he will.
#### Step 1.
Alice deploys a smart contract on Ethereum and locks her ETH in it. The contract has the following properties:
- it is non-destructible
- it contains two timestamps, `t_0` and `t_1`, before and after which different actions are authorized.
- it is constructed containing `P_ed_a` & `P_ed_b`, so that if Alice or Bob reveals their secret by calling the contract, the contract will verify that the secret corresponds to the expected public key that it was initalized with.
- it has a `Ready()` function which can only be called by Alice. Once `Ready()` is invoked, Bob can proceed with redeeming his ether. Alice has until the `t_0` timestamp to call `Ready()` - once `t_0` passes, then the contract automatically allows Bob to claim his ether, up until some second timestamp `t_1`.
- it has a `Claim()` function which can only be called by Bob after `Ready()` is called or `t_0` passes, up until the timestamp `t_1`. After `t_1`, Bob can no longer claim the ETH.
- `Claim()` takes one parameter from Bob: `s_b`. Once `Claim()` is called, the ETH is transferred to Bob, and simultaneously Bob reveals his secret and thus Alice can claim her XMR by combining her and Bob's secrets.
- it has a `Refund()` function that can only be called by Alice and only before `Ready()` is called *or* `t_0` is reached. Once `Ready()` is invoked, Alice can no longer call `Refund()` until the next timestamp `t_1`. If Bob doesn't claim his ether by `t_1`, then `Refund()` can be called by Alice once again.
- `Refund()` takes one parameter from Alice: `s_a`. This allows Alice to get her ETH back in case Bob goes offline, but it simulteneously reveals her secret, allowing Bob to regain access to the XMR he locked.
#### Step 2.
Bob sees the smart contract has been deployed with the correct parameters. He sends his XMR to an account address constructed from `P_a + P_b`. Thus, the funds can only be accessed by an entity having both `s_a` & `s_b`, as the secret spend key to that account is `s_a + s_b`. The funds are viewable by someone having `v_a + v_b`.
Note: `Refund()` and `Claim()` cannot be called at the same time. This is to prevent the case of front-running where, for example, Bob tries to claim, so his secret `s_b` is in the mempool, and then Alice tries to call `Refund()` with a higher priority while also transferring the XMR in the account controlled by `s_a + s_b`. If her call goes through before Bob's and Bob doesn't notice this happening in time, then Alice will now have *both* the ETH and the XMR. Due to this case, Alice and Bob should not call `Refund()` or `Claim()` when they are approaching `t_0` or `t_1` respectively, as their transaction may not go through in time.
#### Step 3.
Alice sees that the XMR has been locked, and the amount is correct (as she knows `v_a` and Bob send her `v_b` in the first key exchange step). She calls `Ready()` on the smart contract if the XMR has been locked. If the amount of XMR locked is incorrect, Alice calls `Refund()` to abort the swap and reclaim her ETH.
From this point on, Bob can redeem his ether by calling `Claim(s_b)`, which transfers the ETH to him.
By redeeming, Bob reveals his secret. Now Alice is the only one that has both `s_a` & `s_b` and she can access the monero in the account created from `P_ed_a + P_ed_b`.
#### What could go wrong
- **Alice locked her ETH, but Bob doesn't lock his XMR**. Alice has until time `t_0` to call `Refund()` to reclaim her ETH, which she should do if `t_0` is soon.
- **Alice called `Ready()`, but Bob never redeems.** Deadlocks are prevented thanks to a second timelock `t_1`, which re-enables Alice to call refund after it, while disabling Bob's ability to claim.
- **Alice never calls `ready` within `t_0`**. Bob can still claim his ETH by waiting until after `t_0` has passed, as the contract automatically allows him to call `Claim()`.
Please see the [protocol documentation](docs/protocol.md) for how it works.
## Instructions

View File

@@ -115,8 +115,8 @@ func (s *swapState) generateKeys() (*monero.PublicKeyPair, error) {
// setBobKeys sets Bob's public spend key (to be stored in the contract) and Bob's
// private view key (used to check XMR balance before calling Ready())
func (s *swapState) setBobKeys(sk *monero.PublicKey, vk *monero.PrivateViewKey) {
s.bobSpendKey = sk
s.bobViewKey = vk
s.bobPublicSpendKey = sk
s.bobPrivateViewKey = vk
}
// deployAndLockETH deploys an instance of the Swap contract and locks `amount` ether in it.
@@ -125,29 +125,33 @@ func (s *swapState) deployAndLockETH(amount uint64) (ethcommon.Address, error) {
return ethcommon.Address{}, errors.New("public keys aren't set")
}
if s.bobSpendKey == nil {
if s.bobPublicSpendKey == nil || s.bobPrivateViewKey == nil {
return ethcommon.Address{}, errors.New("bob's keys aren't set")
}
pkAlice := s.pubkeys.SpendKey().Bytes()
pkBob := s.bobSpendKey.Bytes()
var pka, pkb [32]byte
copy(pka[:], pkAlice)
copy(pkb[:], pkBob)
hb := s.bobClaimHash
ha := s.privkeys.SpendKey().Hash()
log.Debug("locking amount: ", amount)
// TODO: put auth in swapState
s.alice.auth.Value = big.NewInt(int64(amount))
defer func() {
s.alice.auth.Value = nil
}()
address, _, swap, err := swap.DeploySwap(s.alice.auth, s.alice.ethClient, pkb, pka, defaultTimeoutDuration)
address, tx, swap, err := swap.DeploySwap(s.alice.auth, s.alice.ethClient, hb, ha, s.bobAddress, defaultTimeoutDuration)
if err != nil {
return ethcommon.Address{}, err
}
receipt, err := s.alice.ethClient.TransactionReceipt(s.ctx, tx.Hash())
if err != nil {
return ethcommon.Address{}, err
}
log.Debugf("deployed Swap.sol, gas used=%d", receipt.CumulativeGasUsed)
balance, err := s.alice.ethClient.BalanceAt(s.ctx, address, nil)
if err != nil {
return ethcommon.Address{}, err
@@ -192,15 +196,12 @@ func (s *swapState) watchForClaim() (<-chan *monero.PrivateKeyPair, error) { //n
for {
select {
case claim := <-ch:
if claim == nil || claim.S == nil {
if claim == nil {
continue
}
// got Bob's secret
sbBytes := claim.S.Bytes()
var sb [32]byte
copy(sb[:], sbBytes)
sb := claim.S
skB, err := monero.NewPrivateSpendKey(sb[:])
if err != nil {
log.Error("failed to convert Bob's secret into a key: ", err)
@@ -233,7 +234,8 @@ func (s *swapState) watchForClaim() (<-chan *monero.PrivateKeyPair, error) { //n
// If time t_1 passes and Claim() has not been called, Alice should call Refund().
func (s *swapState) refund() (string, error) {
secret := s.privkeys.SpendKeyBytes()
sc := big.NewInt(0).SetBytes(secret)
var sc [32]byte
copy(sc[:], secret)
log.Infof("attempting to call Refund()...")
tx, err := s.contract.Refund(s.alice.auth, sc)
@@ -241,6 +243,12 @@ func (s *swapState) refund() (string, error) {
return "", err
}
receipt, err := s.alice.ethClient.TransactionReceipt(s.ctx, tx.Hash())
if err != nil {
return "", err
}
log.Debugf("called Refund(), gas used=%d", receipt.CumulativeGasUsed)
return tx.Hash().String(), nil
}
@@ -249,7 +257,6 @@ func (s *swapState) createMoneroWallet(kpAB *monero.PrivateKeyPair) (monero.Addr
t := time.Now().Format("2006-Jan-2-15:04:05")
walletName := fmt.Sprintf("alice-swap-wallet-%s", t)
if err := s.alice.client.GenerateFromKeys(kpAB, walletName, ""); err != nil {
// TODO: save the keypair on disk!!! otherwise we lose the keys
return "", err
}
@@ -292,12 +299,9 @@ func (s *swapState) handleNotifyClaimed(txHash string) (monero.Address, error) {
return "", err
}
log.Debug("got Bob's secret: ", hex.EncodeToString(res[0].(*big.Int).Bytes()))
// got Bob's secret
sbBytes := res[0].(*big.Int).Bytes()
var sb [32]byte
copy(sb[:], sbBytes)
sb := res[0].([32]byte)
log.Debug("got Bob's secret: ", hex.EncodeToString(sb[:]))
skB, err := monero.NewPrivateSpendKey(sb[:])
if err != nil {
@@ -306,7 +310,7 @@ func (s *swapState) handleNotifyClaimed(txHash string) (monero.Address, error) {
}
skAB := monero.SumPrivateSpendKeys(skB, s.privkeys.SpendKey())
vkAB := monero.SumPrivateViewKeys(s.bobViewKey, s.privkeys.ViewKey())
vkAB := monero.SumPrivateViewKeys(s.bobPrivateViewKey, s.privkeys.ViewKey())
kpAB := monero.NewPrivateKeyPair(skAB, vkAB)
// write keys to file in case something goes wrong

View File

@@ -2,6 +2,7 @@ package alice
import (
"context"
"encoding/hex"
"errors"
"fmt"
"time"
@@ -9,12 +10,16 @@ import (
"github.com/noot/atomic-swap/monero"
"github.com/noot/atomic-swap/net"
"github.com/noot/atomic-swap/swap-contract"
ethcommon "github.com/ethereum/go-ethereum/common"
)
var nextID uint64 = 0
var (
errMissingKeys = errors.New("did not receive Bob's public spend or private view key")
errMissingKeys = errors.New("did not receive Bob's public spend or private view key")
errMissingSpendKeyHash = errors.New("did not receive Bob's spend key hash")
errMissingAddress = errors.New("did not receive Bob's address")
)
// swapState is an instance of a swap. it holds the info needed for the swap,
@@ -33,8 +38,10 @@ type swapState struct {
pubkeys *monero.PublicKeyPair
// Bob's keys for this session
bobSpendKey *monero.PublicKey
bobViewKey *monero.PrivateViewKey
bobPublicSpendKey *monero.PublicKey
bobPrivateViewKey *monero.PrivateViewKey
bobClaimHash [32]byte
bobAddress ethcommon.Address
// swap contract and timeouts in it; set once contract is deployed
contract *swap.Swap
@@ -76,9 +83,12 @@ func (s *swapState) SendKeysMessage() (*net.SendKeysMessage, error) {
return nil, err
}
sh := s.privkeys.SpendKey().Hash()
return &net.SendKeysMessage{
PublicSpendKey: kp.SpendKey().Hex(),
PublicViewKey: kp.ViewKey().Hex(),
PrivateViewKey: s.privkeys.ViewKey().Hex(),
SpendKeyHash: hex.EncodeToString(sh[:]),
}, nil
}
@@ -212,7 +222,30 @@ func (s *swapState) handleSendKeysMessage(msg *net.SendKeysMessage) (net.Message
return nil, errMissingKeys
}
log.Debug("got Bob's keys")
if msg.SpendKeyHash == "" {
return nil, errMissingSpendKeyHash
}
if msg.EthAddress == "" {
return nil, errMissingAddress
}
// TODO: check that hash can be derived to view key
hb, err := hex.DecodeString(msg.SpendKeyHash)
if err != nil {
return nil, err
}
if len(hb) != 32 {
return nil, errors.New("invalid spend key hash")
}
copy(s.bobClaimHash[:], hb)
s.bobAddress = ethcommon.HexToAddress(msg.EthAddress)
log.Debug("got Bob's keys and address: address=%s", s.bobAddress)
s.nextExpectedMessage = &net.NotifyXMRLock{}
sk, err := monero.NewPublicKeyFromHex(msg.PublicSpendKey)

View File

@@ -10,9 +10,12 @@ import (
"github.com/noot/atomic-swap/monero"
"github.com/noot/atomic-swap/net"
logging "github.com/ipfs/go-log"
"github.com/stretchr/testify/require"
)
var _ = logging.SetLogLevel("alice", "debug")
type mockNet struct {
msg net.Message
}
@@ -46,14 +49,16 @@ func TestSwapState_HandleProtocolMessage_SendKeysMessage(t *testing.T) {
msg = &net.SendKeysMessage{
PublicSpendKey: bobPrivKeys.SpendKey().Public().Hex(),
PrivateViewKey: bobPrivKeys.ViewKey().Hex(),
SpendKeyHash: "17e799afa82d5210fd6d41e1b1cb64784c10d72a34ada97807a4533a30627f01",
EthAddress: "0x",
}
resp, done, err := s.HandleProtocolMessage(msg)
require.NoError(t, err)
require.False(t, done)
require.NotNil(t, resp)
require.Equal(t, time.Second*time.Duration(defaultTimeoutDuration.Int64()), s.t1.Sub(s.t0))
require.Equal(t, bobPrivKeys.SpendKey().Public().Hex(), s.bobSpendKey.Hex())
require.Equal(t, bobPrivKeys.ViewKey().Hex(), s.bobViewKey.Hex())
require.Equal(t, bobPrivKeys.SpendKey().Public().Hex(), s.bobPublicSpendKey.Hex())
require.Equal(t, bobPrivKeys.ViewKey().Hex(), s.bobPrivateViewKey.Hex())
}
func TestSwapState_HandleProtocolMessage_SendKeysMessage_Refund(t *testing.T) {
@@ -77,6 +82,8 @@ func TestSwapState_HandleProtocolMessage_SendKeysMessage_Refund(t *testing.T) {
msg := &net.SendKeysMessage{
PublicSpendKey: bobPrivKeys.SpendKey().Public().Hex(),
PrivateViewKey: bobPrivKeys.ViewKey().Hex(),
SpendKeyHash: "17e799afa82d5210fd6d41e1b1cb64784c10d72a34ada97807a4533a30627f01",
EthAddress: "0x",
}
resp, done, err := s.HandleProtocolMessage(msg)
@@ -85,13 +92,14 @@ func TestSwapState_HandleProtocolMessage_SendKeysMessage_Refund(t *testing.T) {
require.NotNil(t, resp)
require.Equal(t, net.NotifyContractDeployedType, resp.Type())
require.Equal(t, time.Second*time.Duration(defaultTimeoutDuration.Int64()), s.t1.Sub(s.t0))
require.Equal(t, bobPrivKeys.SpendKey().Public().Hex(), s.bobSpendKey.Hex())
require.Equal(t, bobPrivKeys.ViewKey().Hex(), s.bobViewKey.Hex())
require.Equal(t, bobPrivKeys.SpendKey().Public().Hex(), s.bobPublicSpendKey.Hex())
require.Equal(t, bobPrivKeys.ViewKey().Hex(), s.bobPrivateViewKey.Hex())
// ensure we refund before t0
time.Sleep(time.Second * 2)
require.NotNil(t, s.net.(*mockNet).msg)
require.Equal(t, net.NotifyRefundType, s.net.(*mockNet).msg.Type())
// TODO: check balance
}
func TestSwapState_NotifyXMRLock(t *testing.T) {

View File

@@ -3,7 +3,6 @@ package bob
import (
"context"
"crypto/ecdsa"
"fmt"
"math/big"
"sync"
@@ -37,6 +36,7 @@ type bob struct {
ethPrivKey *ecdsa.PrivateKey
auth *bind.TransactOpts
callOpts *bind.CallOpts
ethAddress ethcommon.Address
net net.MessageSender
@@ -63,6 +63,7 @@ func NewBob(ctx context.Context, moneroEndpoint, moneroDaemonEndpoint, ethEndpoi
}
pub := pk.Public().(*ecdsa.PublicKey)
addr := crypto.PubkeyToAddress(*pub)
return &bob{
ctx: ctx,
@@ -72,9 +73,10 @@ func NewBob(ctx context.Context, moneroEndpoint, moneroDaemonEndpoint, ethEndpoi
ethPrivKey: pk,
auth: auth,
callOpts: &bind.CallOpts{
From: crypto.PubkeyToAddress(*pub),
From: addr,
Context: ctx,
},
ethAddress: addr,
}, nil
}
@@ -191,10 +193,7 @@ func (s *swapState) watchForRefund() (<-chan *monero.PrivateKeyPair, error) { //
}
// got Alice's secret
saBytes := refund.S.Bytes()
var sa [32]byte
copy(sa[:], saBytes)
sa := refund.S
skA, err := monero.NewPrivateSpendKey(sa[:])
if err != nil {
log.Info("failed to convert Alice's secret into a key: %w", err)
@@ -278,7 +277,8 @@ func (s *swapState) claimFunds() (string, error) {
// call swap.Swap.Claim() w/ b.privkeys.sk, revealing Bob's secret spend key
secret := s.privkeys.SpendKeyBytes()
sc := big.NewInt(0).SetBytes(secret)
var sc [32]byte
copy(sc[:], secret)
tx, err := s.contract.Claim(s.bob.auth, sc)
if err != nil {
@@ -286,16 +286,18 @@ func (s *swapState) claimFunds() (string, error) {
}
log.Info("success! Bob claimed funds")
log.Info("tx hash: ", tx.Hash())
log.Info("tx hash=%s", tx.Hash())
receipt, err := s.bob.ethClient.TransactionReceipt(s.ctx, tx.Hash())
if err != nil {
return "", err
}
//log.Info("tx logs: ", fmt.Sprintf("0x%x", receipt.Logs[0].Data))
log.Info("included in block number: ", receipt.Logs[0].BlockNumber)
log.Info("secret: ", fmt.Sprintf("%x", secret))
log.Infof("included in block number=%d gas used=%d s_a=%x",
receipt.Logs[0].BlockNumber,
receipt.CumulativeGasUsed,
secret,
)
balance, err = s.bob.ethClient.BalanceAt(s.ctx, addr, nil)
if err != nil {

View File

@@ -5,7 +5,6 @@ import (
"encoding/hex"
"errors"
"fmt"
"math/big"
"strings"
"time"
@@ -43,7 +42,8 @@ type swapState struct {
t0, t1 time.Time
// Alice's keys for this session
alicePublicKeys *monero.PublicKeyPair
alicePublicKeys *monero.PublicKeyPair
alicePrivateViewKey *monero.PrivateViewKey
// next expected network message
nextExpectedMessage net.Message
@@ -79,9 +79,13 @@ func (s *swapState) SendKeysMessage() (*net.SendKeysMessage, error) {
return nil, err
}
sh := s.privkeys.SpendKey().Hash()
return &net.SendKeysMessage{
PublicSpendKey: sk.Hex(),
PrivateViewKey: vk.Hex(),
SpendKeyHash: hex.EncodeToString(sh[:]),
EthAddress: s.bob.ethAddress.String(),
}, nil
}
@@ -247,14 +251,27 @@ func (s *swapState) HandleProtocolMessage(msg net.Message) (net.Message, bool, e
}
func (s *swapState) handleSendKeysMessage(msg *net.SendKeysMessage) error {
if msg.PublicSpendKey == "" || msg.PublicViewKey == "" {
if msg.PublicSpendKey == "" || msg.PrivateViewKey == "" {
return errMissingKeys
}
if msg.SpendKeyHash == "" {
return errors.New("did not receive SpendKeyHash")
}
// TODO: verify hash derives view key, and that view only wallet can be generated
log.Debug("got Alice's public keys")
s.nextExpectedMessage = &net.NotifyContractDeployed{}
kp, err := monero.NewPublicKeyPairFromHex(msg.PublicSpendKey, msg.PublicViewKey)
vk, err := monero.NewPrivateViewKeyFromHex(msg.PrivateViewKey)
if err != nil {
return fmt.Errorf("failed to generate Alice's private view key: %w", err)
}
s.alicePrivateViewKey = vk
kp, err := monero.NewPublicKeyPairFromHex(msg.PublicSpendKey, vk.Public().Hex())
if err != nil {
return fmt.Errorf("failed to generate Alice's public keys: %w", err)
}
@@ -284,13 +301,10 @@ func (s *swapState) handleRefund(txHash string) (monero.Address, error) {
return "", err
}
log.Debug("got Alice's secret: ", hex.EncodeToString(res[0].(*big.Int).Bytes()))
sa := res[0].([32]byte)
log.Debug("got Alice's secret: ", hex.EncodeToString(sa[:]))
// got Alice's secret
sbBytes := res[0].(*big.Int).Bytes()
var sa [32]byte
copy(sa[:], sbBytes)
skA, err := monero.NewPrivateSpendKey(sa[:])
if err != nil {
log.Errorf("failed to convert Alice's secret into a key: %s", err)

View File

@@ -15,9 +15,12 @@ import (
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
logging "github.com/ipfs/go-log"
"github.com/stretchr/testify/require"
)
var _ = logging.SetLogLevel("bob", "debug")
type mockNet struct {
msg net.Message
}
@@ -36,10 +39,7 @@ func newTestBob(t *testing.T) (*bob, *swapState) {
bobAddr, err := bob.client.GetAddress(0)
require.NoError(t, err)
err = bob.daemonClient.GenerateBlocks(bobAddr.Address, 61)
require.NoError(t, err)
//time.Sleep(time.Second * 5)
_ = bob.daemonClient.GenerateBlocks(bobAddr.Address, 61)
swapState := newSwapState(bob, 33, 33)
return bob, swapState
@@ -70,9 +70,8 @@ func TestSwapState_ClaimFunds(t *testing.T) {
bob.auth, err = bind.NewKeyedTransactorWithChainID(pkBob, big.NewInt(common.GanacheChainID))
require.NoError(t, err)
var pubkey [32]byte
copy(pubkey[:], swapState.pubkeys.SpendKey().Bytes())
swapState.contractAddr, _, swapState.contract, err = swap.DeploySwap(bob.auth, conn, pubkey, [32]byte{}, defaultTimeoutDuration)
claimHash := swapState.privkeys.SpendKey().Hash()
swapState.contractAddr, _, swapState.contract, err = swap.DeploySwap(bob.auth, conn, claimHash, [32]byte{}, bob.ethAddress, defaultTimeoutDuration)
require.NoError(t, err)
_, err = swapState.contract.SetReady(bob.auth)
@@ -96,7 +95,8 @@ func TestSwapState_handleSendKeysMessage(t *testing.T) {
msg = &net.SendKeysMessage{
PublicSpendKey: alicePrivKeys.SpendKey().Public().Hex(),
PublicViewKey: alicePrivKeys.ViewKey().Public().Hex(),
PrivateViewKey: alicePrivKeys.ViewKey().Hex(),
SpendKeyHash: "01fc704e28a5323372019db199a1c9adfd460eee382f3ee582371323ba62b62e",
}
err = s.handleSendKeysMessage(msg)
@@ -106,15 +106,14 @@ func TestSwapState_handleSendKeysMessage(t *testing.T) {
require.Equal(t, alicePubKeys.ViewKey().Hex(), s.alicePublicKeys.ViewKey().Hex())
}
func deploySwap(t *testing.T, bob *bob, swapState *swapState, timeout time.Duration) (ethcommon.Address, *swap.Swap) {
func deploySwap(t *testing.T, bob *bob, swapState *swapState, refundHash [32]byte, timeout time.Duration) (ethcommon.Address, *swap.Swap) {
conn, err := ethclient.Dial(common.DefaultEthEndpoint)
require.NoError(t, err)
tm := big.NewInt(int64(timeout.Seconds()))
var pubkey [32]byte
copy(pubkey[:], swapState.pubkeys.SpendKey().Bytes())
addr, _, contract, err := swap.DeploySwap(bob.auth, conn, pubkey, [32]byte{}, tm)
claimHash := swapState.privkeys.SpendKey().Hash()
addr, _, contract, err := swap.DeploySwap(bob.auth, conn, claimHash, refundHash, bob.ethAddress, tm)
require.NoError(t, err)
return addr, contract
}
@@ -138,7 +137,7 @@ func TestSwapState_HandleProtocolMessage_NotifyContractDeployed_ok(t *testing.T)
duration, err := time.ParseDuration("2s")
require.NoError(t, err)
addr, _ := deploySwap(t, bob, s, duration)
addr, _ := deploySwap(t, bob, s, [32]byte{}, duration)
msg = &net.NotifyContractDeployed{
Address: addr.String(),
@@ -175,7 +174,7 @@ func TestSwapState_HandleProtocolMessage_NotifyContractDeployed_timeout(t *testi
duration, err := time.ParseDuration("1s")
require.NoError(t, err)
addr, _ := deploySwap(t, bob, s, duration)
addr, _ := deploySwap(t, bob, s, [32]byte{}, duration)
msg = &net.NotifyContractDeployed{
Address: addr.String(),
@@ -202,10 +201,12 @@ func TestSwapState_HandleProtocolMessage_NotifyReady(t *testing.T) {
_, _, err := s.generateKeys()
require.NoError(t, err)
duration, err := time.ParseDuration("1s")
duration, err := time.ParseDuration("10m")
require.NoError(t, err)
_, s.contract = deploySwap(t, bob, s, [32]byte{}, duration)
_, err = s.contract.SetReady(bob.auth)
require.NoError(t, err)
_, s.contract = deploySwap(t, bob, s, duration)
time.Sleep(duration)
msg := &net.NotifyReady{}
@@ -230,7 +231,7 @@ func TestSwapState_handleRefund(t *testing.T) {
duration, err := time.ParseDuration("10m")
require.NoError(t, err)
_, s.contract = deploySwap(t, bob, s, duration)
_, s.contract = deploySwap(t, bob, s, aliceKeys.SpendKey().Hash(), duration)
// lock XMR
_, err = s.lockFunds(s.providesAmount)
@@ -238,7 +239,9 @@ func TestSwapState_handleRefund(t *testing.T) {
// call refund w/ Alice's spend key
secret := aliceKeys.SpendKeyBytes()
sc := big.NewInt(0).SetBytes(secret)
var sc [32]byte
copy(sc[:], secret)
tx, err := s.contract.Refund(s.bob.auth, sc)
require.NoError(t, err)
@@ -259,7 +262,7 @@ func TestSwapState_HandleProtocolMessage_NotifyRefund(t *testing.T) {
duration, err := time.ParseDuration("10m")
require.NoError(t, err)
_, s.contract = deploySwap(t, bob, s, duration)
_, s.contract = deploySwap(t, bob, s, aliceKeys.SpendKey().Hash(), duration)
// lock XMR
_, err = s.lockFunds(s.providesAmount)
@@ -267,7 +270,9 @@ func TestSwapState_HandleProtocolMessage_NotifyRefund(t *testing.T) {
// call refund w/ Alice's spend key
secret := aliceKeys.SpendKeyBytes()
sc := big.NewInt(0).SetBytes(secret)
var sc [32]byte
copy(sc[:], secret)
tx, err := s.contract.Refund(s.bob.auth, sc)
require.NoError(t, err)

60
docs/protocol.md Normal file
View File

@@ -0,0 +1,60 @@
# Protocol
## Current version
See [this issue describing the update](https://github.com/noot/atomic-swap/issues/36).
```
gas used now to deploy Swap.sol: 640005
gas used previously to deploy Swap.sol: 1855645
improvement: ~2.9x
gas used now for the Claim() or Refund() call: 14729
gas used previously for the Claim() or Refund() call: 938818
improvement: ~64x
```
## Initial version
Alice has ETH and wants XMR, Bob has XMR and wants ETH. They come to an agreement to do the swap and the amounts they will swap.
#### Initial (offchain) phase
- Alice and Bob each generate Monero secret keys (which consist of secret spend and view keys): (`s_a`, `v_a`) and (`s_b`, `v_b`), which are used to construct valid points on the ed25519 curve (ie. public keys): `P_a` and `P_b` accordingly. Alice sends Bob her public key and Bob sends Alice his public spend key and private view key. Note: The XMR will be locked in the account with address corresponding to the public key `P_a + P_b`. Bob needs to send his private view key so Alice can check that Bob actually locked the amount of XMR he claims he will.
#### Step 1.
Alice deploys a smart contract on Ethereum and locks her ETH in it. The contract has the following properties:
- it is non-destructible
- it contains two timestamps, `t_0` and `t_1`, before and after which different actions are authorized.
- it is constructed containing `P_a` and`P_b`, so that if Alice or Bob reveals their secret by calling the contract, the contract will verify that the secret corresponds to the expected public key that it was initalized with.
- it has a `Ready()` function which can only be called by Alice. Once `Ready()` is invoked, Bob can proceed with redeeming his ether. Alice has until the `t_0` timestamp to call `Ready()` - once `t_0` passes, then the contract automatically allows Bob to claim his ether, up until some second timestamp `t_1`.
- it has a `Claim()` function which can only be called by Bob after `Ready()` is called or `t_0` passes, up until the timestamp `t_1`. After `t_1`, Bob can no longer claim the ETH.
- `Claim()` takes one parameter from Bob: `s_b`. Once `Claim()` is called, the ETH is transferred to Bob, and simultaneously Bob reveals his secret and thus Alice can claim her XMR by combining her and Bob's secrets.
- it has a `Refund()` function that can only be called by Alice and only before `Ready()` is called *or* `t_0` is reached. Once `Ready()` is invoked, Alice can no longer call `Refund()` until the next timestamp `t_1`. If Bob doesn't claim his ether by `t_1`, then `Refund()` can be called by Alice once again.
- `Refund()` takes one parameter from Alice: `s_a`. This allows Alice to get her ETH back in case Bob goes offline, but it simulteneously reveals her secret, allowing Bob to regain access to the XMR he locked.
#### Step 2.
Bob sees the smart contract has been deployed with the correct parameters. He sends his XMR to an account address constructed from `P_a + P_b`. Thus, the funds can only be accessed by an entity having both `s_a` and `s_b`, as the secret spend key to that account is `s_a + s_b`. The funds are viewable by someone having `v_a + v_b`.
Note: `Refund()` and `Claim()` cannot be called at the same time. This is to prevent the case of front-running where, for example, Bob tries to claim, so his secret `s_b` is in the mempool, and then Alice tries to call `Refund()` with a higher priority while also transferring the XMR in the account controlled by `s_a + s_b`. If her call goes through before Bob's and Bob doesn't notice this happening in time, then Alice will now have *both* the ETH and the XMR. Due to this case, Alice and Bob should not call `Refund()` or `Claim()` when they are approaching `t_0` or `t_1` respectively, as their transaction may not go through in time.
#### Step 3.
Alice sees that the XMR has been locked, and the amount is correct (as she knows `v_a` and Bob send her `v_b` in the first key exchange step). She calls `Ready()` on the smart contract if the XMR has been locked. If the amount of XMR locked is incorrect, Alice calls `Refund()` to abort the swap and reclaim her ETH.
From this point on, Bob can redeem his ether by calling `Claim(s_b)`, which transfers the ETH to him.
By redeeming, Bob reveals his secret. Now Alice is the only one that has both `s_a` and `s_b` and she can access the monero in the account created from `P_a + P_b`.
#### What could go wrong
- **Alice locked her ETH, but Bob doesn't lock his XMR**. Alice has until time `t_0` to call `Refund()` to reclaim her ETH, which she should do if `t_0` is soon.
- **Alice called `Ready()`, but Bob never redeems.** Deadlocks are prevented thanks to a second timelock `t_1`, which re-enables Alice to call refund after it, while disabling Bob's ability to claim.
- **Alice never calls `ready` within `t_0`**. Bob can still claim his ETH by waiting until after `t_0` has passed, as the contract automatically allows him to call `Claim()`.

View File

@@ -1,33 +0,0 @@
// SPDX-License-Identifier: MIT
// Adapted from https://github.com/witnet/elliptic-curve-solidity/blob/master/examples/Secp256k1.sol
// This file is not being used as of the final Hackathon submission
pragma solidity ^0.8.5;
import "./EllipticCurve.sol";
/**
* @title Ed25519 Elliptic Curve
* @notice Particularization of Elliptic Curve for ed25519 curve
*/
contract Ed25519 {
uint256 public constant GX =
15112221349535400772501151409588531511454012693041857206046113283949847762202;
uint256 public constant GY =
46316835694926478169428394003475163141307993866256225615783033603165251855960;
uint256 public constant AA =
37095705934669439343138083508754565189542113879843219016388785533085940283555;
uint256 public constant PP =
0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED;
/// @notice Public Key derivation from private key
/// @param privKey The private key
/// @return (qx, qy) The Public Key
function derivePubKey(uint256 privKey)
external
pure
returns (uint256, uint256)
{
return EllipticCurve.ecMul(privKey, GX, GY, AA, PP);
}
}

View File

@@ -1,112 +0,0 @@
// SPDX-License-Identifier: MIT
// Source https://github.com/javgh/ed25519-solidity
pragma solidity ^0.8.5;
// Using formulas from https://hyperelliptic.org/EFD/g1p/auto-twisted-projective.html
// and constants from https://tools.ietf.org/html/draft-josefsson-eddsa-ed25519-03
contract Ed25519 {
uint constant q = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED;
uint constant d = 37095705934669439343138083508754565189542113879843219016388785533085940283555;
// = -(121665/121666)
uint constant Bx = 15112221349535400772501151409588531511454012693041857206046113283949847762202;
uint constant By = 46316835694926478169428394003475163141307993866256225615783033603165251855960;
struct Point {
uint x;
uint y;
uint z;
}
struct Scratchpad {
uint a;
uint b;
uint c;
uint d;
uint e;
uint f;
uint g;
uint h;
}
function inv(uint a) internal view returns (uint invA) {
uint e = q - 2;
uint m = q;
// use bigModExp precompile
assembly {
let p := mload(0x40)
mstore(p, 0x20)
mstore(add(p, 0x20), 0x20)
mstore(add(p, 0x40), 0x20)
mstore(add(p, 0x60), a)
mstore(add(p, 0x80), e)
mstore(add(p, 0xa0), m)
if iszero(staticcall(not(0), 0x05, p, 0xc0, p, 0x20)) {
revert(0, 0)
}
invA := mload(p)
}
}
function ecAdd(Point memory p1,
Point memory p2) internal pure returns (Point memory p3) {
Scratchpad memory tmp;
tmp.a = mulmod(p1.z, p2.z, q);
tmp.b = mulmod(tmp.a, tmp.a, q);
tmp.c = mulmod(p1.x, p2.x, q);
tmp.d = mulmod(p1.y, p2.y, q);
tmp.e = mulmod(d, mulmod(tmp.c, tmp.d, q), q);
tmp.f = addmod(tmp.b, q - tmp.e, q);
tmp.g = addmod(tmp.b, tmp.e, q);
p3.x = mulmod(mulmod(tmp.a, tmp.f, q),
addmod(addmod(mulmod(addmod(p1.x, p1.y, q),
addmod(p2.x, p2.y, q), q),
q - tmp.c, q), q - tmp.d, q), q);
p3.y = mulmod(mulmod(tmp.a, tmp.g, q),
addmod(tmp.d, tmp.c, q), q);
p3.z = mulmod(tmp.f, tmp.g, q);
}
function ecDouble(Point memory p1) internal pure returns (Point memory p2) {
Scratchpad memory tmp;
tmp.a = addmod(p1.x, p1.y, q);
tmp.b = mulmod(tmp.a, tmp.a, q);
tmp.c = mulmod(p1.x, p1.x, q);
tmp.d = mulmod(p1.y, p1.y, q);
tmp.e = q - tmp.c;
tmp.f = addmod(tmp.e, tmp.d, q);
tmp.h = mulmod(p1.z, p1.z, q);
tmp.g = addmod(tmp.f, q - mulmod(2, tmp.h, q), q);
p2.x = mulmod(addmod(addmod(tmp.b, q - tmp.c, q), q - tmp.d, q),
tmp.g, q);
p2.y = mulmod(tmp.f, addmod(tmp.e, q - tmp.d, q), q);
p2.z = mulmod(tmp.f, tmp.g, q);
}
function scalarMultBase(uint s) public view returns (uint, uint) {
Point memory b;
Point memory result;
b.x = Bx;
b.y = By;
b.z = 1;
result.x = 0;
result.y = 1;
result.z = 1;
while (s > 0) {
if (s & 1 == 1) { result = ecAdd(result, b); }
s = s >> 1;
b = ecDouble(b);
}
uint invZ = inv(result.z);
result.x = mulmod(result.x, invZ, q);
result.y = mulmod(result.y, invZ, q);
return (result.x, result.y);
}
}

View File

@@ -1,429 +0,0 @@
// SPDX-License-Identifier: MIT
// Source https://github.com/witnet/elliptic-curve-solidity/blob/master/contracts/EllipticCurve.sol
// This file is not being used as of the final Hackathon submission
pragma solidity ^0.8.5;
/**
* @title Elliptic Curve Library
* @dev Library providing arithmetic operations over elliptic curves.
* This library does not check whether the inserted points belong to the curve
* `isOnCurve` function should be used by the library user to check the aforementioned statement.
* @author Witnet Foundation
*/
library EllipticCurve {
// Pre-computed constant for 2 ** 255
uint256 constant private U255_MAX_PLUS_1 = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
/// @dev Modular euclidean inverse of a number (mod p).
/// @param _x The number
/// @param _pp The modulus
/// @return q such that x*q = 1 (mod _pp)
function invMod(uint256 _x, uint256 _pp) internal pure returns (uint256) {
require(_x != 0 && _x != _pp && _pp != 0, "Invalid number");
uint256 q = 0;
uint256 newT = 1;
uint256 r = _pp;
uint256 t;
while (_x != 0) {
t = r / _x;
(q, newT) = (newT, addmod(q, (_pp - mulmod(t, newT, _pp)), _pp));
(r, _x) = (_x, r - t * _x);
}
return q;
}
/// @dev Modular exponentiation, b^e % _pp.
/// Source: https://github.com/androlo/standard-contracts/blob/master/contracts/src/crypto/ECCMath.sol
/// @param _base base
/// @param _exp exponent
/// @param _pp modulus
/// @return r such that r = b**e (mod _pp)
function expMod(uint256 _base, uint256 _exp, uint256 _pp) internal pure returns (uint256) {
require(_pp!=0, "Modulus is zero");
if (_base == 0)
return 0;
if (_exp == 0)
return 1;
uint256 r = 1;
uint256 bit = U255_MAX_PLUS_1;
assembly {
for { } gt(bit, 0) { }{
r := mulmod(mulmod(r, r, _pp), exp(_base, iszero(iszero(and(_exp, bit)))), _pp)
r := mulmod(mulmod(r, r, _pp), exp(_base, iszero(iszero(and(_exp, div(bit, 2))))), _pp)
r := mulmod(mulmod(r, r, _pp), exp(_base, iszero(iszero(and(_exp, div(bit, 4))))), _pp)
r := mulmod(mulmod(r, r, _pp), exp(_base, iszero(iszero(and(_exp, div(bit, 8))))), _pp)
bit := div(bit, 16)
}
}
return r;
}
/// @dev Converts a point (x, y, z) expressed in Jacobian coordinates to affine coordinates (x', y', 1).
/// @param _x coordinate x
/// @param _y coordinate y
/// @param _z coordinate z
/// @param _pp the modulus
/// @return (x', y') affine coordinates
function toAffine(
uint256 _x,
uint256 _y,
uint256 _z,
uint256 _pp)
internal pure returns (uint256, uint256)
{
uint256 zInv = invMod(_z, _pp);
uint256 zInv2 = mulmod(zInv, zInv, _pp);
uint256 x2 = mulmod(_x, zInv2, _pp);
uint256 y2 = mulmod(_y, mulmod(zInv, zInv2, _pp), _pp);
return (x2, y2);
}
/// @dev Derives the y coordinate from a compressed-format point x [[SEC-1]](https://www.secg.org/SEC1-Ver-1.0.pdf).
/// @param _prefix parity byte (0x02 even, 0x03 odd)
/// @param _x coordinate x
/// @param _aa constant of curve
/// @param _bb constant of curve
/// @param _pp the modulus
/// @return y coordinate y
function deriveY(
uint8 _prefix,
uint256 _x,
uint256 _aa,
uint256 _bb,
uint256 _pp)
internal pure returns (uint256)
{
require(_prefix == 0x02 || _prefix == 0x03, "Invalid compressed EC point prefix");
// x^3 + ax + b
uint256 y2 = addmod(mulmod(_x, mulmod(_x, _x, _pp), _pp), addmod(mulmod(_x, _aa, _pp), _bb, _pp), _pp);
y2 = expMod(y2, (_pp + 1) / 4, _pp);
// uint256 cmp = yBit ^ y_ & 1;
uint256 y = (y2 + _prefix) % 2 == 0 ? y2 : _pp - y2;
return y;
}
/// @dev Check whether point (x,y) is on curve defined by a, b, and _pp.
/// @param _x coordinate x of P1
/// @param _y coordinate y of P1
/// @param _aa constant of curve
/// @param _bb constant of curve
/// @param _pp the modulus
/// @return true if x,y in the curve, false else
function isOnCurve(
uint _x,
uint _y,
uint _aa,
uint _bb,
uint _pp)
internal pure returns (bool)
{
if (0 == _x || _x >= _pp || 0 == _y || _y >= _pp) {
return false;
}
// y^2
uint lhs = mulmod(_y, _y, _pp);
// x^3
uint rhs = mulmod(mulmod(_x, _x, _pp), _x, _pp);
if (_aa != 0) {
// x^3 + a*x
rhs = addmod(rhs, mulmod(_x, _aa, _pp), _pp);
}
if (_bb != 0) {
// x^3 + a*x + b
rhs = addmod(rhs, _bb, _pp);
}
return lhs == rhs;
}
/// @dev Calculate inverse (x, -y) of point (x, y).
/// @param _x coordinate x of P1
/// @param _y coordinate y of P1
/// @param _pp the modulus
/// @return (x, -y)
function ecInv(
uint256 _x,
uint256 _y,
uint256 _pp)
internal pure returns (uint256, uint256)
{
return (_x, (_pp - _y) % _pp);
}
/// @dev Add two points (x1, y1) and (x2, y2) in affine coordinates.
/// @param _x1 coordinate x of P1
/// @param _y1 coordinate y of P1
/// @param _x2 coordinate x of P2
/// @param _y2 coordinate y of P2
/// @param _aa constant of the curve
/// @param _pp the modulus
/// @return (qx, qy) = P1+P2 in affine coordinates
function ecAdd(
uint256 _x1,
uint256 _y1,
uint256 _x2,
uint256 _y2,
uint256 _aa,
uint256 _pp)
internal pure returns(uint256, uint256)
{
uint x = 0;
uint y = 0;
uint z = 0;
// Double if x1==x2 else add
if (_x1==_x2) {
// y1 = -y2 mod p
if (addmod(_y1, _y2, _pp) == 0) {
return(0, 0);
} else {
// P1 = P2
(x, y, z) = jacDouble(
_x1,
_y1,
1,
_aa,
_pp);
}
} else {
(x, y, z) = jacAdd(
_x1,
_y1,
1,
_x2,
_y2,
1,
_pp);
}
// Get back to affine
return toAffine(
x,
y,
z,
_pp);
}
/// @dev Substract two points (x1, y1) and (x2, y2) in affine coordinates.
/// @param _x1 coordinate x of P1
/// @param _y1 coordinate y of P1
/// @param _x2 coordinate x of P2
/// @param _y2 coordinate y of P2
/// @param _aa constant of the curve
/// @param _pp the modulus
/// @return (qx, qy) = P1-P2 in affine coordinates
function ecSub(
uint256 _x1,
uint256 _y1,
uint256 _x2,
uint256 _y2,
uint256 _aa,
uint256 _pp)
internal pure returns(uint256, uint256)
{
// invert square
(uint256 x, uint256 y) = ecInv(_x2, _y2, _pp);
// P1-square
return ecAdd(
_x1,
_y1,
x,
y,
_aa,
_pp);
}
/// @dev Multiply point (x1, y1, z1) times d in affine coordinates.
/// @param _k scalar to multiply
/// @param _x coordinate x of P1
/// @param _y coordinate y of P1
/// @param _aa constant of the curve
/// @param _pp the modulus
/// @return (qx, qy) = d*P in affine coordinates
function ecMul(
uint256 _k,
uint256 _x,
uint256 _y,
uint256 _aa,
uint256 _pp)
internal pure returns(uint256, uint256)
{
// Jacobian multiplication
(uint256 x1, uint256 y1, uint256 z1) = jacMul(
_k,
_x,
_y,
1,
_aa,
_pp);
// Get back to affine
return toAffine(
x1,
y1,
z1,
_pp);
}
/// @dev Adds two points (x1, y1, z1) and (x2 y2, z2).
/// @param _x1 coordinate x of P1
/// @param _y1 coordinate y of P1
/// @param _z1 coordinate z of P1
/// @param _x2 coordinate x of square
/// @param _y2 coordinate y of square
/// @param _z2 coordinate z of square
/// @param _pp the modulus
/// @return (qx, qy, qz) P1+square in Jacobian
function jacAdd(
uint256 _x1,
uint256 _y1,
uint256 _z1,
uint256 _x2,
uint256 _y2,
uint256 _z2,
uint256 _pp)
internal pure returns (uint256, uint256, uint256)
{
if (_x1==0 && _y1==0)
return (_x2, _y2, _z2);
if (_x2==0 && _y2==0)
return (_x1, _y1, _z1);
// We follow the equations described in https://pdfs.semanticscholar.org/5c64/29952e08025a9649c2b0ba32518e9a7fb5c2.pdf Section 5
uint[4] memory zs; // z1^2, z1^3, z2^2, z2^3
zs[0] = mulmod(_z1, _z1, _pp);
zs[1] = mulmod(_z1, zs[0], _pp);
zs[2] = mulmod(_z2, _z2, _pp);
zs[3] = mulmod(_z2, zs[2], _pp);
// u1, s1, u2, s2
zs = [
mulmod(_x1, zs[2], _pp),
mulmod(_y1, zs[3], _pp),
mulmod(_x2, zs[0], _pp),
mulmod(_y2, zs[1], _pp)
];
// In case of zs[0] == zs[2] && zs[1] == zs[3], double function should be used
require(zs[0] != zs[2] || zs[1] != zs[3], "Use jacDouble function instead");
uint[4] memory hr;
//h
hr[0] = addmod(zs[2], _pp - zs[0], _pp);
//r
hr[1] = addmod(zs[3], _pp - zs[1], _pp);
//h^2
hr[2] = mulmod(hr[0], hr[0], _pp);
// h^3
hr[3] = mulmod(hr[2], hr[0], _pp);
// qx = -h^3 -2u1h^2+r^2
uint256 qx = addmod(mulmod(hr[1], hr[1], _pp), _pp - hr[3], _pp);
qx = addmod(qx, _pp - mulmod(2, mulmod(zs[0], hr[2], _pp), _pp), _pp);
// qy = -s1*z1*h^3+r(u1*h^2 -x^3)
uint256 qy = mulmod(hr[1], addmod(mulmod(zs[0], hr[2], _pp), _pp - qx, _pp), _pp);
qy = addmod(qy, _pp - mulmod(zs[1], hr[3], _pp), _pp);
// qz = h*z1*z2
uint256 qz = mulmod(hr[0], mulmod(_z1, _z2, _pp), _pp);
return(qx, qy, qz);
}
/// @dev Doubles a points (x, y, z).
/// @param _x coordinate x of P1
/// @param _y coordinate y of P1
/// @param _z coordinate z of P1
/// @param _aa the a scalar in the curve equation
/// @param _pp the modulus
/// @return (qx, qy, qz) 2P in Jacobian
function jacDouble(
uint256 _x,
uint256 _y,
uint256 _z,
uint256 _aa,
uint256 _pp)
internal pure returns (uint256, uint256, uint256)
{
if (_z == 0)
return (_x, _y, _z);
// We follow the equations described in https://pdfs.semanticscholar.org/5c64/29952e08025a9649c2b0ba32518e9a7fb5c2.pdf Section 5
// Note: there is a bug in the paper regarding the m parameter, M=3*(x1^2)+a*(z1^4)
// x, y, z at this point represent the squares of _x, _y, _z
uint256 x = mulmod(_x, _x, _pp); //x1^2
uint256 y = mulmod(_y, _y, _pp); //y1^2
uint256 z = mulmod(_z, _z, _pp); //z1^2
// s
uint s = mulmod(4, mulmod(_x, y, _pp), _pp);
// m
uint m = addmod(mulmod(3, x, _pp), mulmod(_aa, mulmod(z, z, _pp), _pp), _pp);
// x, y, z at this point will be reassigned and rather represent qx, qy, qz from the paper
// This allows to reduce the gas cost and stack footprint of the algorithm
// qx
x = addmod(mulmod(m, m, _pp), _pp - addmod(s, s, _pp), _pp);
// qy = -8*y1^4 + M(S-T)
y = addmod(mulmod(m, addmod(s, _pp - x, _pp), _pp), _pp - mulmod(8, mulmod(y, y, _pp), _pp), _pp);
// qz = 2*y1*z1
z = mulmod(2, mulmod(_y, _z, _pp), _pp);
return (x, y, z);
}
/// @dev Multiply point (x, y, z) times d.
/// @param _d scalar to multiply
/// @param _x coordinate x of P1
/// @param _y coordinate y of P1
/// @param _z coordinate z of P1
/// @param _aa constant of curve
/// @param _pp the modulus
/// @return (qx, qy, qz) d*P1 in Jacobian
function jacMul(
uint256 _d,
uint256 _x,
uint256 _y,
uint256 _z,
uint256 _aa,
uint256 _pp)
internal pure returns (uint256, uint256, uint256)
{
// Early return in case that `_d == 0`
if (_d == 0) {
return (_x, _y, _z);
}
uint256 remaining = _d;
uint256 qx = 0;
uint256 qy = 0;
uint256 qz = 1;
// Double and add algorithm
while (remaining != 0) {
if ((remaining & 1) != 0) {
(qx, qy, qz) = jacAdd(
qx,
qy,
qz,
_x,
_y,
_z,
_pp);
}
remaining = remaining / 2;
(_x, _y, _z) = jacDouble(
_x,
_y,
_z,
_aa,
_pp);
}
return (qx, qy, qz);
}
}

View File

@@ -2,23 +2,18 @@
pragma solidity ^0.8.5;
// import "./Ed25519.sol";
import "./Ed25519_alt.sol";
contract Swap {
// Ed25519 library
Ed25519 immutable ed25519;
// contract creator, Alice
address payable immutable owner;
// the expected public key derived from the secret `s_b`.
// this public key is a point on the ed25519 curve
bytes32 public immutable pubKeyClaim;
// address allowed to claim the ether in this contract
address payable immutable claimer;
// the expected public key derived from the secret `s_a`.
// this public key is a point on the ed25519 curve
bytes32 public immutable pubKeyRefund;
// the expected hash of the secret `s_b`.
bytes32 public immutable claimHash;
// the expected hash of the secret `s_a`.
bytes32 public immutable refundHash;
// timestamp (set at contract creation)
// before which Alice can call either set_ready or refund
@@ -31,19 +26,19 @@ contract Swap {
// this prevents Bob from withdrawing funds without locking funds on the other chain first
bool isReady = false;
event Constructed(bytes32 p);
event Constructed(bytes32 claimHash, bytes32 refundHash);
event IsReady(bool b);
event Claimed(uint256 s);
event Refunded(uint256 s);
event Claimed(bytes32 s);
event Refunded(bytes32 s);
constructor(bytes32 _pubKeyClaim, bytes32 _pubKeyRefund, uint256 _timeoutDuration) payable {
constructor(bytes32 _claimHash, bytes32 _refundHash, address payable _claimer, uint256 _timeoutDuration) payable {
owner = payable(msg.sender);
pubKeyClaim = _pubKeyClaim;
pubKeyRefund = _pubKeyRefund;
claimHash = _claimHash;
refundHash = _refundHash;
claimer = _claimer;
timeout_0 = block.timestamp + _timeoutDuration;
timeout_1 = block.timestamp + (_timeoutDuration * 2);
ed25519 = new Ed25519();
emit Constructed(_pubKeyRefund);
emit Constructed(claimHash, refundHash);
}
// Alice must call set_ready() within t_0 once she verifies the XMR has been locked
@@ -56,11 +51,11 @@ contract Swap {
// Bob can claim if:
// - Alice doesn't call set_ready or refund within t_0, or
// - Alice calls ready within t_0, in which case Bob can call claim until t_1
function claim(uint256 _s) external {
function claim(bytes32 _s) external {
require(msg.sender == claimer, "only claimer can claim!");
require(block.timestamp < timeout_1 && (block.timestamp >= timeout_0 || isReady),
"too late or early to claim!");
verifySecret(_s, pubKeyClaim);
require(keccak256(abi.encode(_s)) == claimHash, "secret is not preimage to claimHash");
emit Claimed(_s);
// send eth to caller (Bob)
@@ -70,26 +65,16 @@ contract Swap {
// Alice can claim a refund:
// - Until t_0 unless she calls set_ready
// - After t_1, if she called set_ready
function refund(uint256 _s) external {
function refund(bytes32 _s) external {
require(msg.sender == owner);
require(
block.timestamp >= timeout_1 || ( block.timestamp < timeout_0 && !isReady),
"It's Bob's turn now, please wait!"
);
verifySecret(_s, pubKeyRefund);
require(keccak256(abi.encode(_s)) == refundHash, "secret is not preimage to refundHash");
emit Refunded(_s);
// send eth back to owner==caller (Alice)
selfdestruct(owner);
}
function verifySecret(uint256 _s, bytes32 pubKey) internal view {
// (uint256 px, uint256 py) = ed25519.derivePubKey(_s);
(uint256 px, uint256 py) = ed25519.scalarMultBase(_s);
uint256 canonical_p = py | ((px % 2) << 255);
require(
bytes32(canonical_p) == pubKey,
"provided secret does not match the expected pubKey"
);
}
}

View File

@@ -205,6 +205,17 @@ func (k *PrivateSpendKey) View() (*PrivateViewKey, error) {
}, nil
}
// Hash returns the keccak256 of the secret key bytes
func (k *PrivateSpendKey) Hash() [32]byte {
return Keccak256(k.key.Bytes())
}
// HashString returns the keccak256 of the secret key bytes as a hex encoded string
func (k *PrivateSpendKey) HashString() string {
h := Keccak256(k.key.Bytes())
return hex.EncodeToString(h[:])
}
type PrivateViewKey struct {
key *ed25519.Scalar
}

View File

@@ -140,15 +140,17 @@ func (m *InitiateMessage) Type() byte {
// SendKeysMessage is sent by both parties to each other to initiate the protocol
type SendKeysMessage struct {
PublicSpendKey string
PublicViewKey string
PrivateViewKey string
SpendKeyHash string
EthAddress string
}
func (m *SendKeysMessage) String() string {
return fmt.Sprintf("SendKeysMessage PublicSpendKey=%s PublicViewKey=%s PrivateViewKey=%v",
return fmt.Sprintf("SendKeysMessage PublicSpendKey=%s PrivateViewKey=%s SpendKeyHash=%s EthAddress=%s",
m.PublicSpendKey,
m.PublicViewKey,
m.PrivateViewKey,
m.SpendKeyHash,
m.EthAddress,
)
}

File diff suppressed because one or more lines are too long

View File

@@ -2,12 +2,13 @@ package swap
import (
"context"
"encoding/hex"
"crypto/ecdsa"
"fmt"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
@@ -19,18 +20,6 @@ import (
var defaultTimeoutDuration = big.NewInt(60) // 60 seconds
func reverse(s []byte) []byte {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
return s
}
func setBigIntLE(s []byte) *big.Int { //nolint
s = reverse(s)
return big.NewInt(0).SetBytes(s)
}
func TestDeploySwap(t *testing.T) {
conn, err := ethclient.Dial(common.DefaultEthEndpoint)
require.NoError(t, err)
@@ -41,25 +30,21 @@ func TestDeploySwap(t *testing.T) {
authAlice, err := bind.NewKeyedTransactorWithChainID(pk_a, big.NewInt(common.GanacheChainID))
require.NoError(t, err)
address, tx, swapContract, err := DeploySwap(authAlice, conn, [32]byte{}, [32]byte{}, defaultTimeoutDuration)
address, tx, swapContract, err := DeploySwap(authAlice, conn, [32]byte{}, [32]byte{}, ethcommon.Address{}, defaultTimeoutDuration)
require.NoError(t, err)
t.Log(address)
t.Log(tx)
t.Log(swapContract)
require.NotEqual(t, ethcommon.Address{}, address)
require.NotNil(t, tx)
require.NotNil(t, swapContract)
}
func TestSwap_Claim(t *testing.T) {
// Alice generates key
keyPairAlice, err := monero.GenerateKeys()
require.NoError(t, err)
pubKeyAlice := keyPairAlice.PublicKeyPair().SpendKey().Bytes()
// Bob generates key
keyPairBob, err := monero.GenerateKeys()
require.NoError(t, err)
pubKeyBob := keyPairBob.PublicKeyPair().SpendKey().Bytes()
secretBob := keyPairBob.SpendKeyBytes()
// setup
@@ -72,7 +57,7 @@ func TestSwap_Claim(t *testing.T) {
require.NoError(t, err)
authAlice, err := bind.NewKeyedTransactorWithChainID(pk_a, big.NewInt(common.GanacheChainID))
authAlice.Value = big.NewInt(10)
authAlice.Value = big.NewInt(1000000000000)
require.NoError(t, err)
authBob, err := bind.NewKeyedTransactorWithChainID(pk_b, big.NewInt(common.GanacheChainID))
require.NoError(t, err)
@@ -86,11 +71,12 @@ func TestSwap_Claim(t *testing.T) {
require.NoError(t, err)
fmt.Println("BobBalanceBefore: ", bobBalanceBefore)
var pkAliceFixed [32]byte
copy(pkAliceFixed[:], reverse(pubKeyAlice))
var pkBobFixed [32]byte
copy(pkBobFixed[:], reverse(pubKeyBob))
contractAddress, deployTx, swap, err := DeploySwap(authAlice, conn, pkBobFixed, pkAliceFixed, defaultTimeoutDuration)
bobPub := pk_b.Public().(*ecdsa.PublicKey)
bobAddr := crypto.PubkeyToAddress(*bobPub)
claimHash := keyPairBob.SpendKey().Hash()
refundHash := keyPairAlice.SpendKey().Hash()
contractAddress, deployTx, swap, err := DeploySwap(authAlice, conn, claimHash, refundHash, bobAddr, defaultTimeoutDuration)
require.NoError(t, err)
fmt.Println("Deploy Tx Gas Cost:", deployTx.Gas())
@@ -100,7 +86,7 @@ func TestSwap_Claim(t *testing.T) {
contractBalance, err := conn.BalanceAt(context.Background(), contractAddress, nil)
require.NoError(t, err)
require.Equal(t, contractBalance, big.NewInt(10))
require.Equal(t, contractBalance, big.NewInt(1000000000000))
txOpts := &bind.TransactOpts{
From: authAlice.From,
@@ -113,10 +99,9 @@ func TestSwap_Claim(t *testing.T) {
}
// Bob tries to claim before Alice has called ready, should fail
s := big.NewInt(0).SetBytes(reverse(secretBob))
fmt.Println("Secret:", hex.EncodeToString(reverse(secretBob)))
fmt.Println("PubKey:", hex.EncodeToString(reverse(pubKeyBob)))
_, err = swap.Claim(txOptsBob, s)
var sb [32]byte
copy(sb[:], secretBob)
_, err = swap.Claim(txOptsBob, sb)
require.Regexp(t, ".*too late or early to claim!", err)
// Alice calls set_ready on the contract
@@ -125,7 +110,7 @@ func TestSwap_Claim(t *testing.T) {
require.NoError(t, err)
// The main transaction that we're testing. Should work
tx, err := swap.Claim(txOptsBob, s)
tx, err := swap.Claim(txOptsBob, sb)
require.NoError(t, err)
// The Swap contract has self destructed: should have no balance AND no bytecode at the address
@@ -138,23 +123,23 @@ func TestSwap_Claim(t *testing.T) {
fmt.Println("Tx details are:", tx.Gas())
// check whether Bob's account balance has increased now
// TODO: check whether Bob's account balance has updated
// bobBalanceAfter, err := conn.BalanceAt(context.Background(), authBob.From, nil)
// fmt.Println("BobBalanceBefore: ", bobBalanceAfter)
// fmt.Println("BobBalanceAfter: ", bobBalanceAfter)
// require.NoError(t, err)
// require.Equal(t, big.NewInt(10), big.NewInt(0).Sub(bobBalanceAfter, bobBalanceBefore))
// expected := big.NewInt(0).Sub(big.NewInt(1000000000000), tx.Cost())
// require.Equal(t, expected, big.NewInt(0).Sub(bobBalanceAfter, bobBalanceBefore))
}
func TestSwap_Refund_Within_T0(t *testing.T) {
// Alice generates key
keyPairAlice, err := monero.GenerateKeys()
require.NoError(t, err)
pubKeyAlice := keyPairAlice.PublicKeyPair().SpendKey().Bytes()
// Bob generates key
keyPairBob, err := monero.GenerateKeys()
require.NoError(t, err)
pubKeyBob := keyPairBob.PublicKeyPair().SpendKey().Bytes()
secretAlice := keyPairAlice.SpendKeyBytes()
@@ -164,6 +149,8 @@ func TestSwap_Refund_Within_T0(t *testing.T) {
pk_a, err := crypto.HexToECDSA(common.DefaultPrivKeyAlice)
require.NoError(t, err)
pk_b, err := crypto.HexToECDSA(common.DefaultPrivKeyBob)
require.NoError(t, err)
authAlice, err := bind.NewKeyedTransactorWithChainID(pk_a, big.NewInt(1337)) // ganache chainID
require.NoError(t, err)
@@ -173,11 +160,11 @@ func TestSwap_Refund_Within_T0(t *testing.T) {
require.NoError(t, err)
fmt.Println("AliceBalanceBefore: ", aliceBalanceBefore)
var pkAliceFixed [32]byte
copy(pkAliceFixed[:], reverse(pubKeyAlice))
var pkBobFixed [32]byte
copy(pkBobFixed[:], reverse(pubKeyBob))
contractAddress, _, swap, err := DeploySwap(authAlice, conn, pkBobFixed, pkAliceFixed, defaultTimeoutDuration)
bobPub := pk_b.Public().(*ecdsa.PublicKey)
bobAddr := crypto.PubkeyToAddress(*bobPub)
claimHash := keyPairBob.SpendKey().Hash()
refundHash := keyPairAlice.SpendKey().Hash()
contractAddress, _, swap, err := DeploySwap(authAlice, conn, claimHash, refundHash, bobAddr, defaultTimeoutDuration)
require.NoError(t, err)
txOpts := &bind.TransactOpts{
@@ -186,8 +173,9 @@ func TestSwap_Refund_Within_T0(t *testing.T) {
}
// Alice never calls set_ready on the contract, instead she just tries to Refund immidiately
s := big.NewInt(0).SetBytes(reverse(secretAlice))
_, err = swap.Refund(txOpts, s)
var sa [32]byte
copy(sa[:], secretAlice)
_, err = swap.Refund(txOpts, sa)
require.NoError(t, err)
// The Swap contract has self destructed: should have no balance AND no bytecode at the address
@@ -204,12 +192,10 @@ func TestSwap_Refund_After_T1(t *testing.T) {
// Alice generates key
keyPairAlice, err := monero.GenerateKeys()
require.NoError(t, err)
pubKeyAlice := keyPairAlice.PublicKeyPair().SpendKey().Bytes()
// Bob generates key
keyPairBob, err := monero.GenerateKeys()
require.NoError(t, err)
pubKeyBob := keyPairBob.PublicKeyPair().SpendKey().Bytes()
secretAlice := keyPairAlice.SpendKeyBytes()
@@ -219,6 +205,8 @@ func TestSwap_Refund_After_T1(t *testing.T) {
pk_a, err := crypto.HexToECDSA(common.DefaultPrivKeyAlice)
require.NoError(t, err)
pk_b, err := crypto.HexToECDSA(common.DefaultPrivKeyBob)
require.NoError(t, err)
authAlice, err := bind.NewKeyedTransactorWithChainID(pk_a, big.NewInt(1337)) // ganache chainID
authAlice.Value = big.NewInt(10)
@@ -228,11 +216,11 @@ func TestSwap_Refund_After_T1(t *testing.T) {
require.NoError(t, err)
fmt.Println("AliceBalanceBefore: ", aliceBalanceBefore)
var pkAliceFixed [32]byte
copy(pkAliceFixed[:], reverse(pubKeyAlice))
var pkBobFixed [32]byte
copy(pkBobFixed[:], reverse(pubKeyBob))
contractAddress, _, swap, err := DeploySwap(authAlice, conn, pkBobFixed, pkAliceFixed, defaultTimeoutDuration)
bobPub := pk_b.Public().(*ecdsa.PublicKey)
bobAddr := crypto.PubkeyToAddress(*bobPub)
claimHash := keyPairBob.SpendKey().Hash()
refundHash := keyPairAlice.SpendKey().Hash()
contractAddress, _, swap, err := DeploySwap(authAlice, conn, claimHash, refundHash, bobAddr, defaultTimeoutDuration)
require.NoError(t, err)
txOpts := &bind.TransactOpts{
@@ -242,11 +230,12 @@ func TestSwap_Refund_After_T1(t *testing.T) {
// Alice calls set_ready on the contract, and immediately tries to Refund
// After waiting T1, Alice should be able to refund now
s := big.NewInt(0).SetBytes(reverse(secretAlice))
_, err = swap.SetReady(txOpts)
require.NoError(t, err)
_, err = swap.Refund(txOpts, s)
var sa [32]byte
copy(sa[:], secretAlice)
_, err = swap.Refund(txOpts, sa)
require.Regexp(t, ".*It's Bob's turn now, please wait!", err)
// wait some, then try again
@@ -256,7 +245,7 @@ func TestSwap_Refund_After_T1(t *testing.T) {
ret := rpcClient.Call(&result, "evm_increaseTime", 3600*25)
require.NoError(t, ret)
_, err = swap.Refund(txOpts, s)
_, err = swap.Refund(txOpts, sa)
require.NoError(t, err)
// The Swap contract has self destructed: should have no balance AND no bytecode at the address