athanorlabs / atomic-swap Goto Github PK
View Code? Open in Web Editor NEWπ« ETH-XMR atomic swap implementation
License: GNU Lesser General Public License v3.0
π« ETH-XMR atomic swap implementation
License: GNU Lesser General Public License v3.0
Anyone can see Bob's secret in the mempool and nothing prevents them from calling claim instead.
The most straightforward way to prevent this would be for Bob to provide the address from which he'll call the contract along with his Monero keys and for Alice to include it in the contract I think.
Then we can restrict claim to only be callable by Bob.
thinking about the design of this project further, this is my current idea/proposal of how it will work.
as I plan to add a discovery layer to the project, there should be a daemon process (swapd
) and a client process (swapcli
) to separate out the network discovery and protocol logic from the user interaction logic.
swapd
components:
dht.FindProviders
; b. communication with peers who we wish to perform the swap protocol with.swapcli
(or potentially a front-end) can use to interact with the daemonswapcli
components/commands:
discover <desired-coin>
: queries the daemon for peers who provide the given coin. optionally, can put amount and exchange rate. returns a list of peers who provide the desired coin, their maximum limit, and exchange rate (and their peer address)initiate <peer> <have-coin> <amount>
: sends a notification to the daemon that we wish to begin the exchange protocol with the given peer, providing the given coin in the given amount. (this will probably need to be implemented with websockets, as the daemon will want to return to us some success or failure message)initiate
). the user can accept or reject. if they accept, the swap proceeds, otherwise nothing happens or a failure is sent to the remote peer.networking specifications (ie. stream protocols);
/atomic-swap/query/0
: when a peer opens this stream with you, they will send you a Query{Coin}
message, asking about the coin you have said you provide, you will respond with a QueryResponse{Coin, MaxAmount, ExchangeRate}
message, giving the peer the desired info. after this, either side is free to close the stream./atomic-swap/protocol/0
: when a peer opens this stream with you, they will send you a message Initiate{ProvidesCoin, ProvidesAmount, DesiredAmount, Keys}
initiating the protocol and sending their keys (as in the first step of the swap protocol). if you accept, you will respond with your keys. otherwise, close the stream.requirements:
any thoughts or feedback is much appreciated :)
investigate extending the ETH contract to allow for direct swaps for ERC20 tokens. this can probably be achieved by firsltly approving the contact to spend some amount of tokens, then transferring them on the Claim
step.
there should be an RPC call to set the query response sent out:
type QueryResponse struct {
Provides []ProvidesCoin
MaximumAmount []uint64
ExchangeRate common.ExchangeRate
}
the RPC call should accept a list of coins, and list of maximum amounts, and the exchange rate (optional)
currently the amount of XMR that will be received is sent in the SendKeysMessage
from the peer, but the ETH holder should actually query the peer for the offer and confirm the amount themselves before initiating (and then the value can be checked upon receiving SendKeysMessage
)
create mainnet, testnet, and dev environments that can be set via cli flag on the daemon.
--regtest
modetodo:
the user should be able to accept or reject incoming swap requests via cli, eg. swapcli accept <swap-id>
or swapcli reject <swap-id>
general error cleanup, should exist in an error.go
file in each package and be a declared variable for better error handling
there is a potential for a bad actor to make a monero swap offer, but without actually having the funds. then some eth holding party may come, lock their funds, and then be forced to eventually refund.
getting the xmr-holder to send a proof of funds (https://www.getmonero.org/resources/developer-guides/wallet-rpc.html#get_reserve_proof) in their initial message, which the counterparty will then verify, prevents the case where the xmr-holder didn't have the funds to begin with. however, it doesn't prevent against the case where they don't lock the funds. it also forces the xmr-holder to reveal the address that initially holds the funds, which is not ideal.
I might not end up implementing this due to the above drawbacks, needs some thought.
Adaptor signatures are used in the XMR/BTC atomic swap reference client
More In Depth Reading HERE
The setup for an adaptor signature involves a secret value, an adaptor signature, and a βnormal" signature. Knowing any two of these data is enough to calculate the third
This allows us to verify the signatures across different curves in a way that secrets can be revealed upon the final redeem transaction.
ed25519 inside of a smart contract is costly. An adaptor signature swap allows for native secp256k1 inside the contract through sha256() and ecrecover() calls. These also cut down on the needed storage for each instance of the swap which reduces cots. This approach also enables ETH to be pre-deposited to a market contract which enables on chain signaling for easier defi integration.
This is based earlier draft
Classic Alice and Bob want to swap eth for xmr
*step 4 is needed to prevent front running attacks since only the withdraw address is able to withdraw. Time lock reverts funds to original funder after x amount of time. Financial incentive makes B claim deposit To further add privacy on the eth side, funds are pooled in the smart contract allowing for an anon set to increase the more atomic swaps that happen. (similar to tornado cash)
Key advantage in this means that no zero-knowledge proofs are needed inside the smart contract as it all happens offchain. This reduces costs significantly and reduces the need for trusted key setups and complexity. The smart contract is basically a simple escrow contract which is locked for a period of time and reverts if nothing happens after x amount of time.
The contract in a locked state can release funds by the withdrew address interacting with it and providing the Ak key shard as a claim key. The transaction that does this also contains a signature so this enables Alice to then calculate the monero priv key K from the shard received earlier (Ak) and adaptor sig + signature in tx to calculate bK.
The contract not only time locks to allow enough time for the swap to happen, but it also locks the address. This is done to prevent the potential of a party brute forcing hashes for AK. This lowers the privacy a bit since the address is on chain in the contract but I think it isn't a big deal since it would be public when they withdrew anyways. In the future it could incorporate some zero knowledge proofs in contract but that's a later conversation.
While I understand the highlevel use of adaptor signatures I am having some trouble implementing them. I know that there are reference clients in rust that were used for the XMR/BTC swap, we can use the same functions for creating the adaptor sigs and for calculating secrets once Bob claims eth deposit. This should seriously cut down on associated costs for the swap as the computational expensive cryptography is done off chain and the on chain contract acts as a simple escrow contract.
Also if there is anything wrong in my foundational understanding of adaptor signatures please call me out on it. I wasn't able to get as much feedback and those specific crypto concepts are not super common.
Golang adaptor sig libary
XMR/BTC atomic swap reference client
XMR/BTC atomic swap POC client
Encrypted Signature whitepaper
currently past swap information is only stored in memory, it would be nice to persist this to disk.
currently, offers aren't persisted to disk, so when a node restarts it has no offers. offers should be persisted to disk so when the node restarts, it reloads all its offers again.
My understanding of block.timestamp is that the value is set by the miner and that its drift regarding the time is not clearly defined.
Therefore wouldn't it be wise to specify a block height as a replacement or an addendum to the timestamps t0 and t1 so the risk of coordinated attacks on the protocol?
Such attacks could be Bob and a cooperating miner moving block.timestamp past t0 to enable a Claim even though Ready was not called by Alice or Alice cooperating with a miner to move block.timestamp past t1 so Refund can be claimed while a Claim is in the mempool and therefore Alice can retrieve her ETH while having access to s_b to get the XMR too.
currently, the first step of the protocol (key exchange phase) does not require either party to go first. however, there is a potential attack where if Bob sends his public key P_b = G*s_b
first, Alice sends back P_a = -P_b + P_a'
where the secret is then -s_b + s_a'
. after Alice locks her eth, Bob then locks his monero in the account P_a + P_b = -P_b + P_a' + P_b = P_a' = G*s_a'
. since Alice knows s_a'
she can unlock the monero as well as the eth.
to prevent this from happening, either Alice can be forced to send keys first (a bit hacky, not preferred), or Alice must send a proof of knowledge of the private key corresponding to P_a
along with her keys. this proof of knowledege can take the form P_a.sign(P_a)
(ie. a signature of P_a
, signed with s_a
). since Alice does not know s_b
this prevents the above attack from occuring.
thanks to @kayabaNerve for pointing this out!
currently the logs are always in debug mode, add a flag to switch the log levels.
currently Swap.sol
verifySecret()
does not work 2/3 of the time within the Go code, investigate this issue and fix it. however, it works with the javascript tests.
if a user hasn't set the contrat to ready yet, or it's timed out, the user should be able the refund eth from the UI
add documentation detailing the DLEq update to the protocol
it would be cool to add tor support to the networking layer: see https://github.com/berty/go-libp2p-tor-transport which is still in development, but maybe one day we can add it
currently, the Alice and Bob protocols don't take into account the timestamps t0 and t1 for when they are able to claim and refund. the protocol needs to be updated to reflect these timestamps (and fetch them from the contract on deployment).
currently the address and private keys of Alice and Bob are hard-coded to ganache's first two determinstic keys. this needs to be updated so that the private keys can be passed in via CLI flag that points to some private key file.
create end-to-end test cases of the swap for all possible claim/refund cases, and assert that in all cases, the swap either occurs successfully and both parties receive funds, or the swap aborts successfully and both parties are refunded.
to save on gas, a secp256k1 ScalarBaseMult can occur in the swap contract instead of a ed25519 ScalarBaseMult to verify the secret corresponding to the public key, as this is cheaper to do with secp256k1. a DLEq proof can be used to then verify that the ed25519 public key and secp256k1 public key correspond to the same secret scalar. this needs to be provided by both Alice and Bob parties, as both their public keys are stored in the contract.
to perform this integration:
SendKeysMessage
, and the swap aborted if it isn't validfrom u/kingofclubstroy on reddit:
I'd suggest reordering the swap structs values in the swap factory smart contract in order to save gas, those bools in between the uint256 values could be moved to the top of the struct to share the same storage slot as the first address parameter (20 bytes + 1 byte/bool < 32 bytes). Also timestamps typically don't need to use 256 bits (32 bytes), and could also be packed into the address storage slots. In the first storage slot with, now with the 2 bools and address, that leaves 10 bytes to play with, 2810 = ~11024, which way more than enough seconds to deal with any time this contract would ever use. So move one timestamp into the first storage slot as a uint80 and the other timestamp into the second storage slot with the other address value. The cost is having to convert the uint80 timestamp to a uint256 when retrieved, but I believe it is worth the gas savings. In total you can reduce the storage slots used by 3, from 8 to 5, a roughly 40% reduction in storage costs for the user.
implement websockets server and create subscriptions for:
currently, the swap daemon requires either private key or wallet access to funds, as it automatically locks funds for you. for the eth side, it requires a private key and for the monero side it requires an unlocked wallet on monero-wallet-rpc.
while this makes the swap experience nicer in some ways as it doesn't require user interaction during the swap, it's not as condusive of an experience within a UI, as it requires the user to run the daemon on the backend and load their keys into it. for a completely in-browser experience, the user would need to interact during the swap by locking funds.
this issue is to implement a mode (specified by a CLI flag) that does not require private keys or wallet access, but instead prompts the user to send funds to some address during the locking stage.
currently, Bob needs to have some ETH to pay for gas when calling Claim()
to get his ETH. this is a pretty old ETH problem (eg. I have DAI but no ETH, so the DAI is trapped) - I'm sure there is likely some existing solution to this, just need to explore what's out there.
there should be a RPC endpoint that return the status of the currently ongoing swap ie. what stage of the swap it's at.
I propose an update to the existing swap protocol that will dramatically reduce the gas costs to deploy the contract and call Claim/Refund.
the current protocol verifies that the secret passed to the contract (either s_a
or s_b
) corresponds to the public key of either Alice or Bob (that was set when the contract was deployed) by performing an ed25519 scalar base multiplication and checking that the resulting point == the public key. this is expensive (around 1mil gas currently)
however, this can be replaced by only verifying a keccak256 hash (30 gas!!)
for background, a monero private view key is derived from the private spend key as follows:
func (k *PrivateSpendKey) View() (*PrivateViewKey, error) {
h := Keccak256(k.key.Bytes())
vk, err := ed25519.NewScalar().SetBytesWithClamping(h[:])
if err != nil {
return nil, err
}
return &PrivateViewKey{
key: vk,
}, nil
}
first, the spend key is hashed using keccak256 and then set to a point on the ed25519 curve using clamping. the resulting scalar is the view key.
the updated protocol is as follows:
P_a
and P_b
) and private view keys (v_a
and v_b
), and additionally the keccak256-hashes of their private spend keys (h_a
and h_b
) that are used to derive their private view keys. when each party receives the other party's keys, they verify that the hash they receive corresponds to the view key.h_a
and h_b
in the contract. Claim can be called by revealing the pre-image to h_b
(ie. s_b
) and Refund can be called by revealing the pre-image to h_a
(ie. s_a
)P_a + P_b
, which is viewable with v_a + v_b
and spendable with s_a + s_b
. either Claim or Refund will be called on the contract, and thus one party will end up with the spend key to the account at completion of the protocol.note: we don't verify in step 1 that the view key corresponds to the public spend key, as it's impossible to do so. however, if the public spend key does not correspond to the view key, then the funds cannot be viewed. before Bob locks his XMR, he can check that he can generate a view-only wallet using the summation of the two view keys. if he cannot, he should abort the protocol as the view keys and public keys do not correspond. or, on Alice's side, if she cannot view the funds after they are locked (as the view keys are wrong), she should abort.
implications:
I'll work on updating the contract/code for this and reply with the benchmark updates. and of course, let me know if there are security issuse with this proposed update.
should push a notification when a new peer is discovered
Alice could potentially deploy a malicious swap contract with the same function signatures as the real contract and fool Bob into using it. Bob should check against this by:
document usage of recovery module in case of program crash during a swap
integrate w/ recovery module
it would be nice to have a method that combines discovery
and query
so that peers and offers can be found in one go.
add rpc endpoint that shows the status of ongoing and/or completed swaps.
there is no check that monerod/monero-wallet-rpc are connected and running on the right network; this should be added otherwise swaps may fail without the issue being obvious.
on the ETH side, if someone decodes the bytecode of the smart contract, they would be able to see that the participants were using a Swap contract and thus Alice now owns some monero.
I'm not sure of a good way around this, but definitely something that needs to be considered.
the peerstore should be persisted to disk so when we restart we don't have to discover all our peers again. this is pretty easy to do with the libp2p ds-badger library/
add go-libp2p-kad-dht for peer discovery, additionally peers can advertise what they provide via Provide
and find providers via FindProviders
for what coin they want.
currently the swap contract needs to be deployed for each swap done. this could be changed to be a "Swap factory" contract that is already deployed on ethereum, that users can call to start a new swap, without needing to deploy the whole contract.
document current JSON-API RPC calls available: net_addresses, net_discover, net_queryPeer, and net_initiate
open_wallet
to open wallet w/ funds instead: https://www.getmonero.org/resources/developer-guides/wallet-rpc.html#open_walletclose_wallet
after to close the view only on Alice's side to create the spend wallet after (or stop_wallet
)?currently, the event subscriptions are a bit janky, we're using the go-ethereum abigen bindings which allow for watching for events, but it seems they don't always fire at the right time. investigate this, maybe it's something we're doing, or it might be a limitation with go-ethereum as their docs seem to indicate this isn't fully implemented.
the current workaround is to poll for events, and to send network messages when some contract function has been called.
a complete failure of this software would be loss of funds. to prevent that, failsafes should be implemented. the user should be able the run the daemon in --recovery
mode, so that funds can be recovered.
possible failure cases (in case of Alice):
Refund()
on the contract. (covered by #11)possible failure cases (in case of Bob):
Ready()
. Bob should then wait until t0 and call Claim()
(covered by #11)in these above cases, user needs to know the contract address - it should be written to disk as soon as it's known. same for Alice and Bob's generated secrets.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.