LocalCryptos has closed

After five successful years, LocalCryptos announced its closure in October 2022. We would like to extend a massive thank you to our users. We greatly appreciate the feedback and support many of you have given us over the years.

LocalCryptos began as a hobby project and experiment in 2017, then called LocalEthereum. We had the idea of using an Ethereum smart contract as a peer-to-peer trading escrow mechanism, rather than a middle-man, and using a new cryptography-in-the-browser technology to power a self-custodial web wallet that can interact with that smart contract.

Before LocalCryptos, there were other cryptography-powered peer-to-peer marketplaces similar to it, but these required users to download an application onto their computer in order to access them, making it near-impossible for self-custodial P2P trading to reach ordinary people and achieve mainstream adoption. LocalCryptos brought those ideas to the web browser so everyone, regardless of technical know-how, had the ability to trade crypto without the risks of hacks and thefts that plague centralized exchanges.

The experiment quickly turned out to be a success. Later, we invented new ways to create functionally similar self-custodial escrow accounts using Bitcoin's Script language, and so we renamed ourselves LocalCryptos, bringing self-custodial P2P trading to Bitcoin and other UTXO-based cryptocurrencies (Litecoin, Dash, and Bitcoin Cash).

While we had much more planned for LocalCryptos, unfortunately we believe that internal and external factors prevent our small team from fully achieving our vision.

This web page serves two purposes:

  • To answer users' important questions and ensure no cryptocurrency is lost in self-custodial wallets
  • To explain our technology in-depth — for interested developers, researchers, and entrepeneurs

1 LocalCryptos has closed2 How can I access my wallet?3 How can I access my history?4 How can I contact LocalCryptos?5 How did LocalCryptos work?5.1 Acknowledgements5.2 Basics of LocalCryptos5.2.1 What was decentralized and what was not?5.2.2 Cryptography inside your browser5.2.3 How account passwords work5.2.3.1 Creating an account5.2.3.2 Logging into an account5.2.3.3 Changing your password5.2.4 Known caveats5.3 Messages5.3.1 Before end-to-end encryption5.3.2 The emergence of end-to-end encryption5.3.3 What's not encrypted5.3.4 Secretly agreeing on an encryption key5.3.5 The internal structure of messages5.3.5.1 Standard messages5.3.5.2 Encrypted attachments5.3.6 Encrypting message payloads5.3.7 Compatibility with payment disputes5.3.8 Limitations and risks5.3.9 Why is there no Double Ratchet?5.4 Bitcoin escrows5.4.1 Why not OP_CHECKMULTISIG?5.4.2 Generating keys in advance5.4.2.1 Generating end-to-end messaging keys5.4.2.2 Generating wallet addresses5.4.2.3 Generating Bitcoin escrow keys5.4.3 Funding escrow5.4.3.1 Escrow output5.4.3.2 Fee output5.4.3.3 Waiting for confirmations5.4.4 Revealing a secret code5.4.5 Using a secret code5.4.6 Recalling the refundable fee output5.4.7 Settle when you spend5.4.8 Releasing without an internet connection5.4.9 How secure is this?5.5 Bitcoin Cash escrows5.5.1 Seller's deposit into escrow5.5.1.1 Escrow output template5.5.1.2 Fee output template5.5.1.3 Script signature5.6 Ethereum escrows5.6.1 Smart contract source code5.6.2 Creating and funding an escrow5.6.3 Avoiding up-front miner fees with a relay system5.6.4 Making changes to the escrow5.6.5 Escrow identifiers5.6.6 The Escrow struct5.6.7 Disputing a trade6 More details

How can I access my wallet?

LocalCryptos had a self-custodial web wallet built into its website. If you backed up your wallet, as recommended, you can access your cryptocurrency using the open source LocalCryptos Wallet Backup Explorer utility. Following the instructions included in the GitHub repository, you will be able to regain access to any stored cryptocurrency in your self-custodial wallets or decentralized escrow accounts.

For those who didn't back up their wallets, we will continue to make our self-custodial web wallet available for you to do so — however you should not rely on it to remain available or bug-free indefinitely. You can access the web wallet here.

How can I access my history?

You can continue to access your trade history here. We encourage you to use the 'Export History' tool located in your Account page.

How can I contact LocalCryptos?

LocalCryptos' customer support ticket system remains available for general inquiries and questions. If you have a general inqury, please open a support ticket.

For inquiries from law enforcement or in relation to our Service Terms or Privacy Policy, we ask that you use the relevant contact method outlined in the footers of our Service Terms or Privacy Policy.

How did LocalCryptos work?

LocalCryptos was a self-custodial application powered by decentralized technology and cryptography. Unlike traditional marketplaces, LocalCryptos never touched users' funds — instead, users exchanged money directly with one another, and cryptocurrency was transferred using decentralized smart contracts and self-custodial wallets.

Now that LocalCryptos has closed, we intend to use this page as a resource to explain the novel technology which underpinned the marketplace.

Acknowledgements

Although LocalCryptos introduced new concepts, it couldn't have existed without the ideas and initiatives of others. In particular, we would especially like to acknowledge the efforts of:

  • LocalBitcoins and Paxful — the two original peer-to-peer cryptocurrency marketplaces
  • Blockchain.com — as the login flow securing the original Blockchain.info encrypted web wallet is what LocalCryptos drew upon to enable users to secure their accounts with memorable passwords
  • The developers and researchers behind the Off-the-Record Messaging (OTR) end-to-end encrypted instant messaging protocol and its many variants
  • Emre Tekisalp, Tristan King, and Siddharth Coelho-Prabhu — who gave us the idea to use Web3 signatures as key material, which made it possible for LocalCryptos users to 'sign in' with third-party Ethereum wallets
  • Pet3rpan and Louis Holbrook for helping to proofread our original technical paper in 2018

Basics of LocalCryptos

One of the major differences between LocalCryptos and other peer-to-peer trading platforms was that LocalCryptos was secured by cryptography and decentralized technology.

LocalCryptos was decentralized in the way that matters most — the marketplace couldn't see your private keys, couldn't access your cryptocurrency, and couldn't read your messages without consent, and it didn't have the power to withdraw from escrow accounts. Additionally, all trades were peer-to-peer and transfers were made off-platform using external payment services, which means the marketplace wasn't trusted with either crypto or dollars.

But like everything you interact with online, LocalCryptos was not a completely decentralized system. It was made up of a mixture of centralized and decentralized components to strike a balance between strong security and usability. The platform was centralized in the same way that end‐to‐end encrypted applications such as Signal, Telegram, Wickr, Viber and WhatsApp each rely on centralized servers to deliver code, transport and store encrypted payloads, and perform housekeeping tasks that would be infeasible or unnecessary to execute in a self-custodial or end-to-end encrypted manner.

Unlike most websites on the internet, many important operations on the marketplace were performed inside your browser rather than by servers. Nevertheless, the marketplace did have servers that were responsible for certain tasks — however their role was as limited as feasibly possible with available technology.

We made the choice to avoid a fully decentralized system to be able to adhere to important design constraints. That meant, primarily, making the product available to everyone, regardless of their knowledge and experience with cryptography and cryptocurrency.

What was decentralized and what was not?

As a quick summary, the following components of LocalCryptos can be summed up as either decentralized or centralized:

  • Wallets: Decentralized — The private keys to wallets are generated by user devices and can never be seen by our servers. The LocalCryptos web wallet is a self-custodial wallet.
  • Escrow: Decentralized — P2P transactions are secured using self-custodial escrow mechanisms that are powered by blockchain technology. The marketplace lacks the power to withdraw from a decentralized escrow account.
  • User messages: End-to-end encrypted — Messages between users were end-to-end encrypted. LocalCryptos can't read message history without permission.
  • Payments: Off-platform — In peer-to-peer transactions, payments are made using external financial services outside the marketplace's environment.
  • Reputation: Centralized — Users' reputation, such as feedback scores and volume, is maintained in a centralized database.
  • Offers: Centralized — Public offers were posted by users to a directory maintained by LocalCryptos, and our servers made searching and sorting fast and easy.
  • Trade metadata: Centralized — Although trade messages and transfers involve cryptography and self-custodial wallets, various metadata properties were not encrypted. For example, amounts, currency codes, and public addresses, were published to a centralized database.
  • Notifications: Centralized — Email notifications, SMS messages, and so on, were delivered to you by servers maintained by LocalCryptos.

Cryptography inside your browser

LocalCryptos was built using the relatively modern Web Cryptography API, which has made it possible to perform cryptographic operations, such as encryption and decryption, from within a web application. As of 2023, Web Cryptography is supported by the latest version of all major desktop and mobile web browsers.

LocalCryptos is among only a few well-known web applications that took full advantage of the Web Cryptography API to build a new category of website backed by cryptography.

Cryptography was used by the LocalCryptos web app in various ways to secure information, and to enable end‐to‐end encrypted messaging. Our goal had always been to allow users to retain complete control over the most important aspects of their account, while only revealing minimal metadata to the marketplace's servers.

How account passwords work

You can log in to your account with a username and password. However, while the log‐in process might look and feel like an ordinary website's, it actually involves complex cryptography under the hood. The password you have is actually a cryptographic passphrase, and it never leaves your device.

The password selected by you when you first created an account was transformed into a cryptographic key using a key-stretching process known as PBKDF2. Then, your "master key" — which essentially unlocks your account — was encrypted using that newly stretched passphrase.

When you log in, your passphrase goes through the same key-stretching process as it did when you signed up, and then it's used to attempt to decrypt your "master key". It's the ownership of the master key that allows you to decrypt other keys, such as the private keys to your wallet or the keys that are used to encrypt messages with other users.

The reason why everything important is encrypted with this "master key", rather than encrypting everything with your passphrase directly, is so that it's possible to later change your password. Changing your password simply involves re-encrypting the master key with a new passphrase (rather than the alternative which would be to re-encrypt everything).

Creating an account

When a user created a LocalCryptos account, a cryptographic key was generated on the device of the user, known as a "master key". This "master key" would later be used to derive other keys — such as a key for encrypting other private keys used to store cryptocurrency, or a key for signing information.

When creating an account, the first step is to generate a securely random 32-byte "master key" (AccountKeyMaster) using the Web Cryptography API.

Once the AccountKeyMaster is created, two more keys can be derived from it:

  1. AccountKeyIdentityPrivate is the function of SHA256(AccountKeyMaster, "identity"). This will be used for digitally signing messages using elliptic curve cryptography.
  2. AccountKeyEnc is the function of SHA256(AccountKeyMaster, "enc"). This key will be used to encrypt other keys using AES symmetric encryption.

The AccountKeyMaster key must be encrypted using the user-inputted password. However, the password alone will not suffice: it must be first transformed into a secure key suitable for encryption using the key derivation function PBKDF2. This process, known as "key stretching", adds complex computational work to make password cracking very difficult. Passphrase is stretched using PBKDF2 using at least 100,000 iterations with the salt PassphraseSalt to create SecretKey.

Once SecretKey is derived, AccountKeyMaster is encrypted using it with AES-256-CBC. The initialization vector (16 random bytes) is kept as SecretIv.

Before the sign up process can be completed, LocalCryptos required a unique message to be signed to prove that you know the private key. The browser obtained the unique message needed to be signed from the LocalCryptos API. This is known as an ephemeral registration "token" and it contains:

  • TokenId — A unique identifier to the registration token.
  • TokenNonce — The message needed to be signed with the new key pair you are about to create.

Once obtained, a digital signature of the registration token is created using the AccountKeyIdentityPrivate key. TokenSignature is an ECDSA signature of the Keccak-256 hash of TokenNonce.

The non-sensitive information is then submitted to the API and a new LocalCryptos account would be created. (Everything marked "secret" below is not uploaded.)

  • Passphrase (secret) — User-inputted plain password.
  • SecretKey (secret) — A more secure 32-byte key derived from stretching Passphrase using PBKDF2 with the salt PassphraseSalt using at least 100,000 iterations.
  • AccountKeyMaster (secret) — A randomly generated 32-byte account master key. This is the seed to everything in your account; it's crucial to keep this secure.
  • AccountKeyIdentityPrivate (secret) — A key that is the SHA-256 hash of the concatenation of AccountKeyMaster and "identity". Used for signing and authentication.
  • AccountKeyEnc (secret) — A key that is the SHA-256 hash of the concatenation of AccountKeyMaster and "enc". Used for encryption.
  • AccountKeyIdentityPublic — Using the secp256k1 curve, an ECDSA public key that corresponds to AccountKeyIdentityPrivate. Uniqueness is enforced here; no two users can have the same AccountKeyIdentityPublic.
  • PassphraseSalt — The random 16-byte salt used for the PBKDF2 process.
  • PassphrasePBKDF2Iterations — The number of PBKDF2 iterations to use to stretch the Passphrase to more secure key material. A higher number results in a slower key stretching process, weakening brute-force attempts.
  • SecretIv — The randomly generated 16-byte initialization vector used to encrypt AccountKeyMaster.
  • SecretCiphertext — The ciphertext of AccountKeyMaster encrypted to SecretKey using AES-256-CBC.
  • TokenSignature — A digital signature of the Keccak-256 hash of TokenNonce by AccountKeyIdentityPrivate, to verify ownership of AccountKeyIdentityPublic.
  • Any other account information (e.g. username and e-mail address).
Logging into an account

Logging into an account requires two-factor authentication. E‐mail is the default two‐factor authentication method and there is optional support for time-synchronized OTP (e.g. apps like Google Authenticator). SMS is not supported as a two-factor method because SMS is not safe.

Two-factor authentication is necessary to protect the encrypted version of AccountKeyMaster (i.e. SecretCiphertext and SecretIv). Although the decryption process has been slowed by the key stretching process, passwords are still theoretically susceptible to brute‐force attacks (especially dictionary‐based attacks on extremely weak passwords). Hiding the key behind a two‐factor method virtually eliminates the hypothetical attack vector of another user obtaining your passphrase by brute-force.

The two-factor authentication process is executed by the API, meaning that there is a small element of trust here. If the database were to be hacked, an attacker could get all the encrypted private keys. Still, these passwords are salted and stretched so rainbow-table attacks aren't possible and other brute-force attacks are very hard. In this hypothetical hack scenario, it would be difficult for the hacker to obtain passwords except those that are very weak (e.g. "password", or a re-used password from a separate compromised database).

This is the log-in process for two-factor authentication by e-mail:

  1. The user enters their username and password.
  2. The username is delivered to the API in a request for the associated encrypted private key. The password is ignored for now.
  3. An e‐mail is sent to the e‐mail address associated with the username, containing a secure link to progress the login request to the next stage.
  4. Once the link in the e-mail is clicked, the original log-in window receives SecretCiphertext, SecretIv, PassphraseSalt, and PassphrasePBKDF2Iterations.
  5. The user‐entered password is salted and stretched as required to derive SecretKey. The stretching process must be exactly the same as the process used when signing up.
  6. If the SecretKey can be used to calculate AccountKeyMaster correctly and hence AccountKeyIdentityPublic, then the login attempt was successful. If not, the user is prompted to try another password and step 5 is repeated.
  7. A nonce provided by the API is signed by AccountKeyIdentityPrivate to prove ownership of the key pair, and a new session is issued.
  8. AccountKeyMaster is stored in the browser's storage for the duration of the session.
Changing your password

Changing an account password involves re-encrypting AccountKeyMaster with a new SecretKey. The AccountKeyMaster key can never be changed, hence neither can the identity key or account encryption key be changed.

Known caveats

There are some known limitations of our implementation to keep in mind.

  • Brute-force attacks — Brute‐force attacks are very difficult because of the key stretching process, but the threat shouldn't be ignored. No matter how large the warnings are, some users are still going to use weak or reused passwords. As further protection, two-factor authentication is mandatory before the log-in process.
  • Two-factor confusion — The fact that second-factor authentication comes before attempting the password is opposite to nearly every other website out there. This inevitably causes some confusion to users.
  • Lost passwords — If you lose your password, there is no way for staff to manually recover access to an account. It's the same as losing the private key to a cryptocurrency wallet. There is nothing we can do to allow you to regain access to an account without the password. Consequently, if you had any cryptocurrency stored in the web wallet attached to your account, we cannot recover it. This is why LocalCryptos strongly recommended to back up your wallet.
  • No recovery from break-ins — Changing the account password after a break‐in has essentially no effect; the account is forever compromised as the master key cannot change. If an account is stolen, the victim must make a new account and start over.

Messages

Messages between users on the LocalCryptos P2P trading platform were end-to-end encrypted. That means that messages were encrypted in transit from the sender's computer to the recipient's, without allowing anyone in between to intercept and read the conversation in transit, including the marketplace.

The platform accomplishes this using the Web Cryptography API, a modern JavaScript API that brings advanced cryptographic operations such as encryption, decryption, signature verification and secure key generation, to web browsers.

Although sending and receiving messages on LocalCryptos might feel the same as any other website, what's going on 'under the hood' on your device's CPU is vastly different to nearly every other website out there.

LocalCryptos was among only a few well-known web applications that implement secure end-to-end encryption technology inside a website, such as WhatsApp Web.

Before end-to-end encryption

Revelations about mass communications surveillance and major database breaches have made consumers more cautious of their privacy on the internet.

Prominent online services have been the subject of intrusions by anonymous hackers. Troves of sensitive information have been stolen from companies such as Apple, TikTok, Target, Equifax, Yahoo, eBay, Ancestry.com, AOL, Adobe, LinkedIn, Microsoft, YouTube, Instagram, Quora, Airtel, Nintendo, Dropbox, T-Mobile, Uber, the United States Postal Service and more in some of the largest data breaches in recent history.

In addition, it was revealed in 2013 that the U.S. government had been secretly collecting data from internet companies. The NSA's PRISM program siphoned data from a broad range of companies including Google, Microsoft, Yahoo, Facebook, YouTube, Skype, and numerous consumer upstream providers.

Early instant messaging apps provided hardly any protection from these types of breaches. Although messages were sent over encrypted channels, the channel was usually between the client and a service provider. The service provider, such as Facebook, Skype, or Google, was able to read the contents of all messages delivered through the service. If the provider was breached, histories of plain‐text messages could be extracted en masse.

The emergence of end-to-end encryption

In response to these threats, developers and cryptographers accelerated research into new techniques to provide security in a modern digital world where neither internet upstream providers or service providers can be trusted. Many widespread instant messaging apps now employ advanced cryptographic techniques to achieve end‐to‐end confidentiality, including OTR, Apple's iMessage, WhatsApp, Signal, Telegram, and even Facebook Messenger.

LocalCryptos applies the techniques of end‐to‐end encryption so that its users' financial details and personal information are kept confidential between the involved parties. Messages between users are end‐to‐end encrypted, meaning that even in the event of a compromised database, no sensitive information such as bank account information could be extracted from historic messages.

The developers and researchers behind the Off‐the‐Record Messaging (OTR) instant messaging protocol inspired the technology behind end-to-end encrypted messages on LocalCryptos.

What's not encrypted

LocalCryptos cannot read the contents of each message without permission from one of the participants, however it can see metadata related to those messages. For example, the server knows when a message was sent, who it was sent to, and whether and when a particular message was read.

If you think of end-to-end encrypted messages as letters, LocalCryptos is the mail service that delivers the envelopes containing each letter. Although we can't read the letters, we need to know where to deliver an envelope.

By handling the envelopes, we can also make some educated guesses about a letter without reading it:

  • If the envelope feels heavier than normal, we can make an educated guess about how large it is by weighing it. However, this is obfuscated by senders padding the letter with blank pages, so we wouldn't be able to determine the exact word count.
  • If the envelope is accompanied by a parcel, it's obvious that you've sent an attachment (e.g. an image or document) rather than a plain letter. Plain messages are usually less than a kilobyte while attachments can be several thousand times larger.

Secretly agreeing on an encryption key

In order for two users to send encrypted messages back and forth, they must both agree on a shared secret to encrypt and decrypt those messages with. However, they can't allow anyone else to see that shared secret key, so passing that key via LocalCryptos' server as an intermediary is out of the question.

The LocalCryptos application used an anonymous key agreement scheme known as an Elliptic Curve Diffie–Hellman Key Exchange (ECDH). This cryptographic scheme allows two people with public key pairs to derive the same shared secret using one person's private key and the other's public key.

For this to work, LocalCryptos acts a key server to distribute cryptographic public keys to users. (Users' web browsers also remembered the public keys of other users on their device's storage to mitigate against a breach of the key server.)

Users must already know the other's public identity keys or fetch them from the LocalCryptos key server. These identity key pairs, which never change, aren't used in the ECDH process; however they are used to authenticate another set of key pairs that are used in the Diffie–Hellman method.

In addition to the public identity keys, users silently generate a large number of cryptographically secure 'pre keys' (also 'maker keys') when they first register on the platform. These are ephemeral public-private key pairs that are digitally signed using the user identity keys, then uploaded to the LocalCryptos key server. The purpose of these 'pre keys' is to enable a totally asynchronous key exchange (i.e. allow users to receive messages while offline).

When a user initiates a conversation, they will generate a third ephemeral key pair on the fly. We call this the 'taker key'. This key pair is the last piece of the puzzle to enable the secure and asynchronous key agreement scheme to work flawlessly.

The step-by-step process for initiating a secure end-to-end encryption protocol is:

  1. The user initiating the conversation (Alice) grabs the recipient's (Bob's) key information from the LocalCryptos key server. This includes:
    • Bob's identity key (public key)
    • One of Bob's pre-signed 'pre keys' (public key and digital signature)
  2. Alice will verify that the digital signature attached to Bob's 'pre key' was signed by Bob's identity key.
  3. Alice will perform a Diffie–Hellman using the private key of her 'taker key' and the public key of Bob's 'pre key'. The output from this algorithm will become the shared secret (SharedSecretRoot).
  4. The SharedSecretRoot is used as the seed to calculate more secure keys using a key derivation function. Two further keys are derived from the root for different purposes:
    • SharedSecretEnc — For message encryption using AES‐256
    • SharedSecretMac — For message authentication using HMAC‐SHA256
  5. Alice will provide to Bob via LocalCryptos her public key and attached digital signature (signed with her identity key) of the 'taker key' she generated during this procedure.

When Bob obtains her public key, he'll have enough information to be able to discover the same shared secret Alice had computed.

The Diffie–Hellman method produces the same shared secret when given reversed public and private key inputs. For any two private keys pk and p'k and their corresponding ECDSA public keys pu and p'u:

The shared secret (SharedSecretRoot) is the ECDH function using the parameters of one party's private key and the other's public key. For maker key pair mk and mu and taker key pair tk and tu, the shared secret root sr is:

This means that when Bob logs in to check his messages:

  1. Bob will receive Alice's 'taker key' and digital signature, the ciphertext of the encrypted message she sent, and her identity key.
  2. Bob will verify that the digital signature attached to Alice's 'taker key' was signed using her identity key pair to verify that it truly belongs to her.
  3. Bob will perform a Diffie–Hellman using the private key of his 'pre key' and the public key of Alice's 'taker key'. This will provide Bob the same SharedSecretRoot as Alice had generated.
  4. Using the same key derivation functions, Bob will find SharedSecretEnc and SharedSecretMac and use these to decrypt the encrypted message sent by Alice.

The internal structure of messages

When a user writes a message, it's compiled into a JSON-encoded payload before the encryption process begins. There are two types of message payloads: plain-text messages and attachments. Messages are simple plain‐text messages and attachments are for larger file uploads, for example images or documents.

A message payload is a simple JSON structure. There are three properties that must be included in every payload regardless of the type:

Standard messages
  • type — This can be "message" or "attachment".
  • trade_id — The unique identifier of this trade. This is to prevent complex replay attacks (i.e. copying messages from other trades and pretending they were meant for this one.)
  • client_timestamp — The current timestamp on the sender's computer (ISO 8601). This is also to detect complex replay attacks and it allows the arbitrator to call out a bad actor if they claim to have sent a message later or earlier than reality.

Here's an example:

{
  "type": "message",
  "client_timestamp": "2022-01-13T00:00:00Z",
  "trade_id": "xxxx-xxxx-xxxx-xxx",
  "message": "Hello! This is my encrypted message."
}
Encrypted attachments

Sometimes users want to send each other attachments, for example images and documents.

However, including the attachment contents inside the encrypted message could result in a painful user experience, especially for users with poor internet connections. That would mean that the recipient would need to download the entire attachment (which could be several megabytes) before it can even know that it is an attachment.

Instead, we separate the two: the file is encrypted and uploaded separately, and the encrypted message simply points to the location of the encrypted attachment and instructions on how to decrypt it. This gives the recipient the choice to not download the attachment.

The process of sending an attachment is:

  1. The sender generates a fresh secret 256-bit key and initialization vector for encrypting the attachment (AttachmentKey and AttachmentIv).
  2. The sender encrypts the contents of the attachment using AES256‐CBC, with AttachmentKey and AttachmentIv.
  3. An SHA‐256 hash of AttachmentCiphertext is generated to protect the integrity of the attachment (AttachmentHash).
  4. The sender uploads AttachmentCiphertext to cloud storage and is given a unique identifier (AttachmentBlobKey).
  5. The sender constructs a message payload containing a link to the attachment and the information necessary to authenticate and decrypt it (AttachmentBlobKey, AttachmentKey, AttachmentIv, and AttachmentHash). They also include metadata, including the original file name, extension, and size of the attachment.

Here's an example:

{
  "type": "attachment",
  "client_timestamp": "2022-01-13T00:00:00Z",
  "trade_id": "xxxx-xxxx-xxxx-xxx",
  "attachment_blob_key": "AttachmentBlobKey",
  "attachment_sha256": "AttachmentHash",
  "attachment_iv": "AttachmentIv",
  "attachment_key": "AttachmentKey",
  "filename": "UploadedFile.jpg",
  "filesize": 1000000
}

Encrypting message payloads

These JSON-encoded message payloads are encrypted using the conversation's shared secret, signed using the sender's account identity key, and hashed using the shared MAC key. This enables the other user to verify that the message was sent by the sender and hasn't been tampered with along the way.

Here's how that works:

  1. The sender creates an ECDSA signature (MessageSignature) of a hash of MessagePayload.
  2. The sender generates an initialization vector for encryption (MessageIv).
  3. A JSON‐encoded package (MessagePackage) containing MessagePayload and MessageSignature is created.
  4. MessagePackage is encrypted using AES256‐CBC to SharedSecretEnc with IV MessageIv and padding (MessageCiphertext).
  5. To protect the integrity of the message, the sender produces a message authentication code (MessageMac) as HMAC‐SHA256(SharedSecretMac, MessageCiphertext).
  6. The sender submits MessageCiphertext, MessageIv, and MessageMac to the API, which delivers it to the recipient.

Compatibility with payment disputes

There is no such thing as an encrypted messaging protocol that can make it impossible for the recipient of a message to share it with somebody else. You can't prevent the person you're sending a message to from sharing your message, just as you can't stop someone from taking a screenshot. If somebody decides to share their conversation, including with us, they can do so.

To enable the marketplace to fully investigate claims of fraud or abuse, or deal with payment disputes, users could share the contents of a conversation. Sharing the shared secret of a conversation with us allows someone to go back and decrypt the transcript for that particular conversation, however it doesn't impact the privacy of your other conversations. This is because each trade uses a unique set of key pairs to derive that shared secret, making shared secrets also unique.

Limitations and risks

There are some experts who advocate against building web-based encryption applications that are designed to respect the user's interests rather than the web provider's, which is exactly what we've done. While we ultimately disagree with this blanket recommendation because we believe the benefits outweigh the risks and web-based security will improve over time, the arguments against using the Web Cryptography API in websites are not without merit.

The primary argument against using cryptography within web browsers is that web-applications are ultimately less secure than programs and browser extensions when it comes to delivering application code. This is due to the way web browsing works: when you visit this site or any other, your web browser will fetch LocalCryptos' latest application code directly from LocalCryptos' web server and run it—without doing much to authenticate that the code is truly the official LocalCryptos code and hasn't been modified.

You might think that using HTTPS solves this authentication problem, making it so that LocalCryptos can digitally sign the code it delivers, and you are partially correct, however only to an certain extent. While HTTPS practically eliminates the vast majority of theoretical man-in-the-middle attacks against web users, it doesn't solve for the threat that the LocalCryptos web server could itself be compromised. An intelligent hacker could break into LocalCryptos' web servers and carefully modify one of the JavaScript files delivered to end-users, perhaps to insert a few lines of code to siphon private keys.

LocalCryptos did a lot to minimize that risk, including utilizing all the HTTP, DNS, and BGP security mechanisms available. However, it's not possible to totally eliminate that threat. The same can be said about literally every other website and web application out there; there is always the possibility of any website or application being vulnerable to a highly sophisticated attack. At the end of the day, cybersecurity is always a sliding scale that requires a nuanced and honest discussion, and anyone who tells you that a certain piece of software can't be hacked is always wrong.

So what would happen to the privacy of your sent and received private messages if the LocalCryptos website suffered a major hack? It really depends. If your web browser executes maliciously poisoned code injected by a sophisticated hacker, there is a possibility that they could gain access to your private key material and decrypt your conversation history. However, if your web browser doesn't execute that hypothetically poisoned code, then your secret conversations would remain secret regardless of the website's breach. In other words, if you visit the site and log in during an active attack, you could be at risk; however, if you're not actively visiting the website at the time of the attack you wouldn't be at any risk. That's because your web browser only executes a website's code when you intentionally navigate to it.

By comparison, if a centralized messaging service was hacked, everyone would be at risk instantaneously. The attacker would be able to download the plain-text of every sent and received message, because every message would simply be stored in a database in plain-text. The fallout from an attack on an unencrypted messaging service would be far more devastating than an attack on an end-to-end encrypted web application.

In the end, we believe the security benefits of using end-to-end encryption technology on the client-side greatly outweigh the risk of a targeted and sophisticated attack, and that the security of application delivery in web-applications will improve over time as new security technologies become available to modern browsers.

Why is there no Double Ratchet?

While some implementations of end-to-end encrypted messaging such as Signal take key renewals a step further with a Diffie–Hellman agreement after each message roundtrip, that practice doesn't benefit LocalCryptos' implementation and is actually counterproductive since it complicates the dispute resolution process.

Message‐level key renewals are unnecessary for trades since the sessions don't last for long. It makes sense to keep exchanging new keys for long‐lived conversations (e.g. WhatsApp or Facebook conversations that can go on for years), but most trades are over very quickly.

Bitcoin escrows

For Bitcoin escrows, a Bitcoin Script was used that relied on a commit-and-reveal scheme to safely and securely escrow funds without a custodial intermediary.

This Script was used for other UTXO cryptocurrencies, such as Litecoin and Dash, but not Bitcoin Cash. However, in the following explanation we will often refer to just Bitcoin for simplicity.

When the seller put funds into an escrow account, they would generate a P2SH address that would implement complex spending conditions, allowing the funds to end up in the hands of the buyer in the case of a successful trade, or back in the hands of the seller in the case of an agreeable cancellation. Most importantly, it allows a third party arbitrator the ability to allow the buyer or seller claim the output, but makes it impossible for the arbitrator to take the cryptocurrency for themselves.

Why not OP_CHECKMULTISIG?

LocalCryptos didn't use traditional multi-signature outputs, which rely on the OP_CHECKMULTISIG op code.

In a multi-signature Script, each signer is a party to the spending transaction. Under the LocalCryptos model, the arbitrator never has to sign any part of the transaction—even in a payment dispute. In true peer-to-peer fashion, the marketplace is never a signing party to the transaction, which makes it impossible for it to impose spending conditions on escrows—such as how and when the receiving party can spend Bitcoin.

Our mechanism is especially useful for in-person exchanges. Using this mechanism, it would be simple to build out a feature that enables users to release Bitcoin from escrow accounts without access to the internet. For example, it is possible to build software that allows a user to trigger a release by sending an SMS with a unique code, or by showing a QR to the buyer.

Generating keys in advance

Whenever someone registered a LocalCryptos account, they would generate a large number of cryptographic keys and corresponding signatures. Of course, this happens entirely in the background, hidden from the end-user to not interupt the seamless user experience.

These ephemeral keys come in three categories, two of which are relevant to Bitcoin escrows.

Generating end-to-end messaging keys

The unrelated category of random keys created at sign-up are end-to-end encryption keys. These keys allowed people to begin encrypted conversations with you while you're offline. LocalCryptos borrows this idea from popular forward-secret encrypted messaging apps such as Signal and OTR.

Generating wallet addresses

When you generated a Bitcoin address in your self-custodial LocalCryptos wallet, you also uploaded a signed version to LocalCryptos' servers. When the web wallet was setting itself up, it created and signed hundreds of Bitcoin addresses.

This allowed others to open trades with you and fund escrows, even while you were offline. Users fetched one of your addresses from us, and checked the signature against your public key. Doing so helps avoid the risk of sophisticated man-in-the-middle attacks.

Generating Bitcoin escrow keys

Bitcoin 'escrow keys' are 32-byte secret codes which you can reveal later. You generate a hash of the secret code (using hash160) and sign the hash. Then, you upload the hashed code, signature, and an encrypted secret code to the marketplace.

Funding escrow

To put BTC in escrow, the seller creates a Bitcoin transaction containing two outputs. One output is for the amount being escrowed, and the other is the marketplace's refundable fee.

Before doing so, the seller needs to fetch some details from the marketplace:

  1. Hashed escrow key codes from the buyer, arbitrator, and seller (their own).
  2. A signature from the buyer they can use to authenticate the hashed escrow key code.
  3. A hashed public key from the buyer, as well as their own.
  4. A signature from the buyer to verify the public key belongs to the buyer.
  5. The arbitrator's hashed public key, so they can send a small fee.

The seller will confirm each of the buyer's signatures are valid before proceeding. An incorrect signature means that a hacker has attempted to tamper with the escrow.

Escrow output

The escrow output, which carries the amount for the buyer, is a P2SH-P2WSH address for the following Script:

OP_DUP OP_1 OP_EQUAL
OP_IF
  # Release by seller
  OP_DROP
  <BuyerPubKeyHash>
  <ReleaseCodeFromSellerHash>
OP_ELSE
  OP_DUP OP_2 OP_EQUAL
  OP_IF
    # Release by arbitrator
    OP_DROP
    <BuyerPubKeyHash>
    <ReleaseCodeFromArbitratorHash>
  OP_ELSE
    OP_DUP OP_3 OP_EQUAL
    OP_IF
      # Return by buyer
      OP_DROP
      <SellerPubKeyHash>
      <ReturnCodeFromBuyerHash>
    OP_ELSE
      OP_4 OP_EQUALVERIFY
      # Return by arbitrator
      <SellerPubKeyHash>
      <ReturnCodeFromArbitratorHash>
    OP_ENDIF
  OP_ENDIF
OP_ENDIF
OP_ROT
OP_HASH160
OP_EQUALVERIFY
OP_OVER
OP_HASH160
OP_EQUALVERIFY
OP_CHECKSIG
Fee output

The fee output carries an amount of Bitcoin approximately 1% of the trade's size. The marketplace will claim the fee if the trade is successful. If there's a cancellation, the seller can unlock the output to claim a full refund.

The fee output is a P2SH-P2WSH address for the folowing Script:

OP_DUP OP_3 OP_EQUAL
OP_IF
  # Return by buyer
  OP_DROP
  OP_HASH160
  <ReturnCodeFromBuyerHash>
  OP_EQUALVERIFY
  OP_DUP
  OP_HASH160
  <SellerPubKeyHash>
  OP_EQUALVERIFY
  OP_CHECKSIG
OP_ELSE
  OP_DUP OP_4 OP_EQUAL
  OP_IF
    # Return by arbitrator
    OP_DROP
    OP_HASH160
    <ReturnCodeFromArbitratorHash>
    OP_EQUALVERIFY
    OP_DUP
    OP_HASH160
    <SellerPubKeyHash>
    OP_EQUALVERIFY
    OP_CHECKSIG
  OP_ELSE
    # Spend by LocalCryptos
    OP_DUP
    OP_HASH160
    <ArbitratorPubKeyHash>
    OP_EQUALVERIFY
    OP_CHECKSIG
  OP_ENDIF
OP_ENDIF
Waiting for confirmations

Marketplaces should in their user-interface require most Bitcoin escrows to have at least one block confirmation, in order to prevent double-spending attacks. Larger transfers may warrant more confirmations depending on the value of the exchange and the seller's risk appetite.

Revealing a secret code

The buyer already has one of the two inputs needed to unlock the escrow transaction: their own public key. The only missing piece is the seller's secret "releaseBySeller" code.

Likewise, the seller is only one input away from unlocking the escrow. If they get their hands on the buyer's secret "returnByBuyer" code, they can recall the amount in escrow.

The arbitrator holds the secret code to the "releaseByArbitrator" and "returnByArbitrator" escrow keys. If a payment dispute arises, the arbitrator can make a resolution by revealing one of the secrets.

This is the crux of the self-custodial escrow system. The hashed script includes the hash of each secret code, but not the secret code itself.

Using a secret code

To spend an escrow output, the receiver needs to compile a signature with the following items:

  1. Signature — A signature of a hash of the transaction. The signature is checked against the below public key to verify only the receiver can spend.
  2. PubKey — The receiver's public key, matching the hashed version in the code.
  3. SecretCode — The revealed escrow code from another party.
  4. Action — One byte to identify the final state of the escrow.

The first two items are identical to spending a regular P2PKH input, and the next two are unique to this Script.

The "action" byte will tell the script which hashes to check. In a standard trade, a buyer will use a "release by seller" code (0x01). If the first item is 0x01, the script will expect the buyer's public key and the seller's release code.

There are four escrow actions, representing all the scenarios of an escrow:

ActionByteExpected codeExpected PubKey
ReleaseBySeller0x01Seller'sBuyer's
ReleaseByArbitrator0x02Arbitrator'sBuyer's
ReturnByBuyer0x03Buyer'sSeller's
ReturnByArbitrator0x04Arbitrator'sSeller's

Recalling the refundable fee output

In the case of an escrow cancellation, the marketplace doesn't (and cannot) take a fee. With a return escrow code, the above signature is also compatible with fee output. The seller can spend the fee output in the same way they spend a recalled escrow UTXO.

Settle when you spend

Clicking "Release" in a LocalCryptos Bitcoin escrow doesn't change anything on the blockchain. It allows the receiving party to spend the coins from escrow (i.e. unlock the UTXO).

The receiving party can choose to spend these coins immediately, or they can wait. In the user-interface, an escrow UTXO will appear in the web wallet next to your regular addresses.

After spending the coins, the escrow's completion is permanently etched into the blockchain.

Releasing without an internet connection

All the buyer needs is the seller's secret code to claim the escrow, it is possible to allow secure trades to be conducted even in instances where the seller lacks an internet connection. The code is too long to write on paper, but it's a perfect length to store in a phone or to display in a QR code.

LocalCryptos never implemented this feature, however it would be possible to enable two features:

  1. Reveal the code by sending it in an SMS to one of our phone numbers. Our servers can compute the code's hash to identify which trade it belongs to, then forward the code to the buyer.
  2. Show a QR code to the buyer. The buyer doesn't need the internet to verify the code; they only need a piece of software that can calculate a hash. This method will enable in-person exchanges when neither has a stable internet connection. This could for example be useful in countries where power outages are a common occurrence.

How secure is this?

The best supercomputer can't break a standard P2PKH address, which uses a single hash. The key space of a hash160 is 160 bits. We use two hash160s: one for the P2PKH part, and one for the hashed secret code. This brings the key space to 320 bits for external attackers. (The math is more nuanced than that, but you get the idea.)

LocalCryptos Bitcoin escrow addresses are virtually impossible to break.

Bitcoin Cash escrows

Bitcoin Cash is a fork of BTC, which means it has similar Bitcoin script smart contract capabilities. Unlike our self-custodial escrow scripts for Bitcoin, Litecoin, and Dash, LocalCryptos' Script for Bitcoin Cash leverages the OP_CHECKDATASIG op code, which is only available in Bitcoin Cash transactions.

A regular user won't notice a difference between this escrow type, as the outcome is virtually identical to LocalCryptos' self-custodial Bitcoin escrow script. However from a programmer's perspective, the self-custodial Bitcoin Cash P2SH script is more simplistic and intuitive, and it comes with some advantages. (The commit-and-reveal Script described above for Bitcoin escrows would also function for Bitcoin Cash.)

As with all escrows on LocalCryptos, it is technically impossible for the marketplace to steal Bitcoin Cash from escrow. The arbitrator only becomes involved when there is a payment dispute, and once involved it only has the ability to allow the BCH to be redeemed by the buyer or seller.

This Script works in a way that is similar to a multi-signature Bitcoin transaction, except that the parties involved don't need to agree upon how and when the Bitcoin Cash outputs are spent. The oracle — which can be the seller, buyer, or arbitrator, depending on the circumstances of the trade — doesn't have the ability to place conditions on the transaction, unlike with traditional multi-signature wallets.

This is due to the fact that with a traditional multi-signature wallet, all parties must sign a full transaction including all outputs and inputs, whereas with a self-custodial escrow transaction that uses OP_CHECKDATASIG, the oracle simply needs to give the winner a signature which they can use at any time to unlock the BCH in any way they choose.

This type of on-chain escrow mechanism gives the buyer and seller the ability to exchange without permission, and the arbitrator the ability to intervene as a self-custodial mediator in the case of a payment dispute.

Bitcoin Cash users and developers will be glad to know that the Permission Software Foundation has built upon LocalCryptos' Bitcoin Cash escrow mechanism, porting it to CashScript and creating developer walk-throughs and implementation examples. You can find this in their GitHub repository.

Seller's deposit into escrow

To move Bitcoin Cash into escrow, the seller generates a transaction with two outputs. One output is the escrow to the buyer and the other is a refundable fee. If the trade is not successful, the fee can be claimed back by the seller.

In ordinary trades, the seller will allow the buyer to spend the escrow output. This doesn't require our intervention. Similarly, if the buyer chooses to cancel the trade on their own accord, the seller can spend the output without our help. The first scenario is a "release" and the second is a "return".

In the event of a payment dispute, an arbitrator can step in and act as a mediator. The arbitrator can only allow either the seller or the buyer to spend, by design. The fee script allows the arbitrator to collect a fee after a released escrow, or for seller to claim a refund if the trade is unsuccessful.

Escrow output template
OP_DUP # We need to use the byte again afterwards
# Get the hashed public keys we need to compare against (ours, and the oracle)
OP_1
OP_EQUAL
OP_IF
<hash160(SellerPubKey)> # Oracle pub key
<hash160(BuyerPubKey)> # Spender pub key
OP_ELSE
OP_DUP
OP_2 # = release from arbitrator
OP_EQUAL
OP_IF
  <hash160(ArbPubKey)> # Oracle pub key
  <hash160(BuyerPubKey)> # Spender pub key
OP_ELSE
  OP_DUP
  OP_3 # = return from buyer
  OP_EQUAL
  OP_IF
    <hash160(BuyerPubKey)> # Oracle pub key
    <hash160(SellerPubKey)> # Spender pub key
  OP_ELSE
    OP_DUP
    OP_4 # = return from arbitrator
    OP_EQUALVERIFY # must be true, else the message is unknown
    <hash160(ArbPubKey)> # Oracle pub key
    <hash160(SellerPubKey)> # Spender pub key
  OP_ENDIF
OP_ENDIF
OP_ENDIF
# Put the hashed public keys on the alt stack
OP_TOALTSTACK
OP_TOALTSTACK # Stack is effectively reset to the input
# On the alt stack we have: [ hash160(SpenderPubKey), hash160(OraclePubKey) ]
<EscrowKey> # Append the nonce to the escrow key to make the message
OP_CAT # Stack is [ ..., <OraclePubKey>, <0x01 || EscrowKey> ]
OP_SWAP # Use this later; verify the oracle public key hash first
OP_DUP
OP_HASH160
OP_FROMALTSTACK # Grab hashed pub key from alt stack
OP_EQUALVERIFY # Public key checks out; now verify the oracle signature
OP_CHECKDATASIGVERIFY # Now verify the sender
OP_DUP
OP_HASH160
OP_FROMALTSTACK
OP_EQUALVERIFY
OP_CHECKSIG
Fee output template
OP_DEPTH # Count stack size
OP_2
OP_EQUAL # Does the input stack only have two items?
OP_IF # If yes, this is the owner collecting fee; simple PKH
  OP_DUP
  OP_HASH160
  <hash160(ArbPubKey)>
  OP_EQUALVERIFY
  OP_CHECKSIG
OP_ELSE # Seller is spending a "returned" (i.e. canceled) escrow
  OP_DUP
  OP_3 # = return from buyer
  OP_EQUAL
  OP_IF
    <hash160(BuyerPubKey)> # Oracle pub key
  OP_ELSE
    OP_DUP
    OP_4 # = return from arbitrator
    OP_EQUALVERIFY # must be true, else the message is unknown
    <hash160(ArbPubKey)> # Oracle pub key
  OP_ENDIF
  <hash160(SellerPubKey)> # Spender pub key
  # Put the hashed public keys on the alt stack
  OP_TOALTSTACK
  OP_TOALTSTACK # Stack is effectively reset to the input
  # On the alt stack we have: [ hash160(SpenderPubKey), hash160(OraclePubKey) ]
  <EscrowKey> # Append the nonce to the escrow key to make the message
  OP_CAT # Stack is [ ..., <OraclePubKey>, <0x01 || EscrowKey> ]
  OP_SWAP # Use this later; verify the oracle public key hash first
  OP_DUP
  OP_HASH160
  OP_FROMALTSTACK # Grab hashed pub key from alt stack
  OP_EQUALVERIFY # Public key checks out; now verify the oracle signature
  OP_CHECKDATASIGVERIFY # Verify the sender
  OP_DUP
  OP_HASH160
  OP_FROMALTSTACK
  OP_EQUALVERIFY
  OP_CHECKSIG
OP_ENDIF
Script signature

To spend an escrow output, the spender must provide in their Bitcoin Cash transaction's script signature:

<Sig> <SpenderPubKey> <OracleSignature> <OraclePubKey> <ActionByte>
# Example: <Sig> <OwnPubkey> <SignatureFromSeller> <SellerPubKey> OP_1
  1. <ActionByte> is a byte corresponding with the situation being executed.
    • 1: Escrow is being released by the seller
    • 2: Escrow is being released by the arbitrator
    • 3: Escrow is being returned by the buyer
    • 4: Escrow is being returned by the arbitrator
  2. <OraclePubKey> is the public key of the person signing the release/return message.
    • 1: <OraclePubKey> = <SellerPubKey>
    • 2: <OraclePubKey> = <ArbPubKey>
    • 3: <OraclePubKey> = <BuyerPubKey>
    • 4: <OraclePubKey> = <ArbPubKey>
  3. <OracleSignature> is a signature from the oracle of ECDSA(<ActionByte> || <EscrowKey>). The <EscrowKey> is unique so that signatures cannot be re-used across escrows.
  4. <SpenderPubKey>is the buyer's public key if a release, otherwise the seller's public key.
  5. <Sig> is the transaction signature from the spender.

Ethereum escrows

Escrows on LocalCryptos were conducted using an open source Ethereum smart contract. Similar to the Bitcoin and Bitcoin Cash mechanisms, it is technically impossible for LocalCryptos to take control of decentralized ETH escrow accounts. The only time it can step is when it receives explicit permission to resolve a payment dispute, and even then it can only direct the escrowed ETH to one of the parties of the trade.

Smart contract source code

The Solidity code is available below.

/**
   Copyright (c) 2021 LocalCryptos.com

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.7.6;

abstract contract Token {
    function transfer(address _to, uint _value) public virtual returns (bool success);
    function transferFrom(address _from, address _to, uint _value) public virtual returns (bool success);
    function virtualapprove(address _spender, uint _value) public virtual returns (bool success);
    function approve(address spender, uint256 value) public virtual returns (bool);
}

/// @title LocalCryptos ETH Escrows
/// @author LocalCryptos
contract LocalCryptosETHEscrows {
    /***********************
    +   Global settings   +
    ***********************/

    // Address of the arbitrator (currently always localethereum staff)
    address public arbitrator;
    // Address of the owner (who can withdraw collected fees)
    address public owner;
    address public inviterAddress;
    // Addresses of the relayers (which addresses are allowed to forward signed instructions from parties)
    mapping (address => bool) public relayers;

    uint32 public requestCancellationMinimumTime;
    // Cumulative balance of collected fees
    uint256 public feesAvailableForWithdraw;

    /***********************
    +  Instruction types  +
    ***********************/

    // Called when the buyer marks payment as sent. Locks funds in escrow
    uint8 constant INSTRUCTION_SELLER_CANNOT_CANCEL = 0x01;
    // Buyer cancelling
    uint8 constant INSTRUCTION_BUYER_CANCEL = 0x02;
    // Seller cancelling
    uint8 constant INSTRUCTION_SELLER_CANCEL = 0x03;
    // Seller requesting to cancel. Begins a window for buyer to object
    uint8 constant INSTRUCTION_SELLER_REQUEST_CANCEL = 0x04;
    // Seller releasing funds to the buyer
    uint8 constant INSTRUCTION_RELEASE = 0x05;
    // Either party permitting the arbitrator to resolve a dispute
    uint8 constant INSTRUCTION_RESOLVE = 0x06;

    /***********************
    +       Events        +
    ***********************/

    event Created(bytes32 indexed _tradeHash);
    event SellerCancelDisabled(bytes32 indexed _tradeHash);
    event SellerRequestedCancel(bytes32 indexed _tradeHash);
    event CancelledBySeller(bytes32 indexed _tradeHash);
    event CancelledByBuyer(bytes32 indexed _tradeHash);
    event Released(bytes32 indexed _tradeHash);
    event DisputeResolved(bytes32 indexed _tradeHash);

    struct Escrow {
        // So we know the escrow exists
        bool exists;
        // This is the timestamp in whic hthe seller can cancel the escrow after.
        // It has two special values:
        // 0 : Permanently locked by the buyer (i.e. marked as paid; the seller can never cancel)
        // 1 : The seller can only request to cancel, which will change this value to a timestamp.
        //     This option is avaialble for complex trade terms such as cash-in-person where a
        //     payment window is inappropriate
        uint32 sellerCanCancelAfter;
        // Cumulative cost of gas incurred by the relayer. This amount will be refunded to the owner
        // in the way of fees once the escrow has completed
        uint128 totalGasFeesSpentByRelayer;
    }

    // Mapping of active trades. The key here is a hash of the trade proprties
    mapping (bytes32 => Escrow) public escrows;

    modifier onlyOwner() {
        require(msg.sender == owner, "Must be owner");
        _;
    }

    modifier onlyArbitrator() {
        require(msg.sender == arbitrator, "Must be arbitrator");
        _;
    }

    /// @notice Initialize the contract.
    constructor() {
        owner = msg.sender;
        arbitrator = msg.sender;
        inviterAddress = msg.sender;
        requestCancellationMinimumTime = 0 seconds;
    }

    /// @notice Create and fund a new escrow.
    /// @param _tradeID The unique ID of the trade, generated by localethereum.com
    /// @param _seller The selling party
    /// @param _buyer The buying party
    /// @param _value The amount of the escrow, exclusive of the fee
    /// @param _fee Localethereum's commission in 1/10000ths
    /// @param _paymentWindowInSeconds The time in seconds from escrow creation that the seller can cancel after
    /// @param _expiry This transaction must be created before this time
    /// @param _v Signature "v" component
    /// @param _r Signature "r" component
    /// @param _s Signature "s" component
    function createEscrow(
        bytes16 _tradeID,
        address _seller,
        address _buyer,
        uint256 _value,
        uint16 _fee,
        uint32 _paymentWindowInSeconds,
        uint32 _expiry,
        uint8 _v,
        bytes32 _r,
        bytes32 _s
    ) payable external {
        // The trade hash is created by tightly-concatenating and hashing properties of the trade.
        // This hash becomes the identifier of the escrow, and hence all these variables must be
        // supplied on future contract calls
        bytes32 _tradeHash = keccak256(abi.encodePacked(_tradeID, _seller, _buyer, _value, _fee));
        // Require that trade does not already exist
        require(!escrows[_tradeHash].exists, "Trade already exists");
        // A signature (v, r and s) must come from localethereum to open an escrow
        bytes32 _invitationHash = keccak256(abi.encodePacked(
            _tradeHash,
            _paymentWindowInSeconds,
            _expiry
        ));
        require(recoverAddress(_invitationHash, _v, _r, _s) == inviterAddress, "Invitation signature was not valid");
        // These signatures come with an expiry stamp
        require(block.timestamp < _expiry, "Signature has expired");
        // Check transaction value against signed _value and make sure is not 0
        require(msg.value == _value && msg.value > 0, "Incorrect ETH sent");
        uint32 _sellerCanCancelAfter = _paymentWindowInSeconds == 0
            ? 1
            : uint32(block.timestamp) + _paymentWindowInSeconds;
        // Add the escrow to the public mapping
        escrows[_tradeHash] = Escrow(true, _sellerCanCancelAfter, 0);
        emit Created(_tradeHash);
    }

    uint16 constant GAS_doResolveDispute = 45368;
    /// @notice Called by the arbitrator to resolve a dispute. Requires a signature from either party.
    /// @param _tradeID Escrow "tradeID" parameter
    /// @param _seller Escrow "seller" parameter
    /// @param _buyer Escrow "buyer" parameter
    /// @param _value Escrow "value" parameter
    /// @param _fee Escrow "fee parameter
    /// @param _v Signature "v" component
    /// @param _r Signature "r" component
    /// @param _s Signature "s" component
    /// @param _buyerPercent What % should be distributed to the buyer (this is usually 0 or 100)
    function resolveDispute(
        bytes16 _tradeID,
        address payable _seller,
        address payable _buyer,
        uint256 _value,
        uint16 _fee,
        uint8 _v,
        bytes32 _r,
        bytes32 _s,
        uint8 _buyerPercent
    ) external onlyArbitrator {
        address _signature = recoverAddress(keccak256(abi.encodePacked(
            _tradeID,
            INSTRUCTION_RESOLVE
        )), _v, _r, _s);
        require(_signature == _buyer || _signature == _seller, "Must be buyer or seller");

        Escrow memory _escrow;
        bytes32 _tradeHash;
        (_escrow, _tradeHash) = getEscrowAndHash(_tradeID, _seller, _buyer, _value, _fee);
        require(_escrow.exists, "Escrow does not exist");
        require(_buyerPercent <= 100, "_buyerPercent must be 100 or lower");

        uint256 _totalFees = _escrow.totalGasFeesSpentByRelayer + (GAS_doResolveDispute * uint128(tx.gasprice));
        require(_value - _totalFees <= _value, "Overflow error"); // Prevent underflow
        feesAvailableForWithdraw += _totalFees; // Add the the pot for localethereum to withdraw

        delete escrows[_tradeHash];
        emit DisputeResolved(_tradeHash);
        if (_buyerPercent > 0)
          payable(_buyer).transfer((_value - _totalFees) * _buyerPercent / 100);
        if (_buyerPercent < 100)
          payable(_seller).transfer((_value - _totalFees) * (100 - _buyerPercent) / 100);
    }

    /// @notice Release ETH in escrow to the buyer. Direct call option.
    /// @param _tradeID Escrow "tradeID" parameter
    /// @param _seller Escrow "seller" parameter
    /// @param _buyer Escrow "buyer" parameter
    /// @param _value Escrow "value" parameter
    /// @param _fee Escrow "fee parameter
    /// @return bool
    function release(
        bytes16 _tradeID,
        address _seller,
        address payable _buyer,
        uint256 _value,
        uint16 _fee
    ) external returns (bool) {
        require(msg.sender == _seller, "Must be seller");
        return doRelease(_tradeID, _seller, _buyer, _value, _fee, 0);
    }

    /// @notice Disable the seller from cancelling (i.e. "mark as paid"). Direct call option.
    /// @param _tradeID Escrow "tradeID" parameter
    /// @param _seller Escrow "seller" parameter
    /// @param _buyer Escrow "buyer" parameter
    /// @param _value Escrow "value" parameter
    /// @param _fee Escrow "fee parameter
    /// @return bool
    function disableSellerCancel(
        bytes16 _tradeID,
        address _seller,
        address _buyer,
        uint256 _value,
        uint16 _fee
    ) external returns (bool) {
        require(msg.sender == _buyer, "Must be buyer");
        return doDisableSellerCancel(_tradeID, _seller, _buyer, _value, _fee, 0);
    }

    /// @notice Cancel the escrow as a buyer. Direct call option.
    /// @param _tradeID Escrow "tradeID" parameter
    /// @param _seller Escrow "seller" parameter
    /// @param _buyer Escrow "buyer" parameter
    /// @param _value Escrow "value" parameter
    /// @param _fee Escrow "fee parameter
    /// @return bool
    function buyerCancel(
      bytes16 _tradeID,
      address payable _seller,
      address _buyer,
      uint256 _value,
      uint16 _fee
    ) external returns (bool) {
        require(msg.sender == _buyer, "Must be buyer");
        return doBuyerCancel(_tradeID, _seller, _buyer, _value, _fee, 0);
    }

    /// @notice Cancel the escrow as a seller. Direct call option.
    /// @param _tradeID Escrow "tradeID" parameter
    /// @param _seller Escrow "seller" parameter
    /// @param _buyer Escrow "buyer" parameter
    /// @param _value Escrow "value" parameter
    /// @param _fee Escrow "fee parameter
    /// @return bool
    function sellerCancel(
        bytes16 _tradeID,
        address payable _seller,
        address _buyer,
        uint256 _value,
        uint16 _fee
    ) external returns (bool) {
        require(msg.sender == _seller, "Must be seller");
        return doSellerCancel(_tradeID, _seller, _buyer, _value, _fee, 0);
    }

    /// @notice Request to cancel as a seller. Direct call option.
    /// @param _tradeID Escrow "tradeID" parameter
    /// @param _seller Escrow "seller" parameter
    /// @param _buyer Escrow "buyer" parameter
    /// @param _value Escrow "value" parameter
    /// @param _fee Escrow "fee parameter
    /// @return bool
    function sellerRequestCancel(
        bytes16 _tradeID,
        address _seller,
        address _buyer,
        uint256 _value,
        uint16 _fee
    ) external returns (bool) {
        require(msg.sender == _seller, "Must be seller");
        return doSellerRequestCancel(_tradeID, _seller, _buyer, _value, _fee, 0);
    }

    uint16 constant GAS_batchRelayBase = 32720;
    /// @notice Relay multiple signed instructions from parties of escrows.
    /// @param _tradeID List of _tradeID values
    /// @param _seller List of _seller values
    /// @param _buyer List of _buyer values
    /// @param _value List of _value values
    /// @param _fee List of _fee values
    /// @param _maximumGasPrice List of _maximumGasPrice values
    /// @param _v List of signature "v" components
    /// @param _r List of signature "r" components
    /// @param _s List of signature "s" components
    /// @param _instructionByte List of _instructionByte values
    /// @return bool List of results
    function batchRelay(
        bytes16[] memory _tradeID,
        address payable[] memory _seller,
        address payable[] memory _buyer,
        uint256[] memory _value,
        uint16[] memory _fee,
        uint128[] memory _maximumGasPrice,
        uint8[] memory _v,
        bytes32[] memory _r,
        bytes32[] memory _s,
        uint8[] memory _instructionByte
    ) public returns (bool[] memory) {
        bool[] memory _results = new bool[](_tradeID.length);
        uint128 _additionalGas = uint128(relayers[msg.sender] == true ? (GAS_batchRelayBase / _tradeID.length) : 0);
        for (uint8 i=0; i<_tradeID.length; i++) {
            _results[i] = relay(
                _tradeID[i],
                _seller[i],
                _buyer[i],
                _value[i],
                _fee[i],
                _maximumGasPrice[i],
                _v[i],
                _r[i],
                _s[i],
                _instructionByte[i],
                _additionalGas
            );
        }
        return _results;
    }

    /// @notice Withdraw fees collected by the contract. Only the owner can call this.
    /// @param _to Address to withdraw fees in to
    /// @param _amount Amount to withdraw
    function withdrawFees(address payable _to, uint256 _amount) onlyOwner external {
        // This check also prevents underflow
        require(_amount <= feesAvailableForWithdraw, "Amount is higher than amount available");
        feesAvailableForWithdraw -= _amount;
        payable(_to).transfer(_amount);
    }

    /// @notice Set the arbitrator to a new address. Only the owner can call this.
    /// @param _newArbitrator Address of the replacement arbitrator
    function setArbitrator(address _newArbitrator) onlyOwner external {
        arbitrator = _newArbitrator;
    }

    /// @notice Change the owner to a new address. Only the owner can call this.
    /// @param _newOwner Address of the replacement owner
    function setOwner(address _newOwner) onlyOwner external {
        owner = _newOwner;
    }

    /// @notice Enable or disable a relayer address. Only the owner can call this.
    /// @param _newRelayer Address of the relayer
    /// @param _enabled Whether the relayer is enabled
    function setRelayer(address _newRelayer, bool _enabled) onlyOwner external {
        relayers[_newRelayer] = _enabled;
    }

    /// @notice Change the inviter to a new address. Only the owner can call this.
    /// @param _newInviterAddress Address of the inviter address
    function setInviterAddress(address _newInviterAddress) onlyOwner external {
        inviterAddress = _newInviterAddress;
    }

    /// @notice Change the requestCancellationMinimumTime. Only the owner can call this.
    /// @param _newRequestCancellationMinimumTime Replacement
    function setRequestCancellationMinimumTime(
        uint32 _newRequestCancellationMinimumTime
    ) onlyOwner external {
        requestCancellationMinimumTime = _newRequestCancellationMinimumTime;
    }

    /// @notice Send ERC20 tokens away. This function allows the owner to withdraw stuck ERC20 tokens.
    /// @param _tokenContract Token contract
    /// @param _transferTo Recipient
    /// @param _value Value
    function transferToken(
        Token _tokenContract,
        address _transferTo,
        uint256 _value
    ) onlyOwner external {
        _tokenContract.transfer(_transferTo, _value);
    }

    /// @notice Send ERC20 tokens away. This function allows the owner to withdraw stuck ERC20 tokens.
    /// @param _tokenContract Token contract
    /// @param _transferTo Recipient
    /// @param _transferFrom Sender
    /// @param _value Value
    function transferTokenFrom(
        Token _tokenContract,
        address _transferTo,
        address _transferFrom,
        uint256 _value
    ) onlyOwner external {
        _tokenContract.transferFrom(_transferTo, _transferFrom, _value);
    }

    /// @notice Send ERC20 tokens away. This function allows the owner to withdraw stuck ERC20 tokens.
    /// @param _tokenContract Token contract
    /// @param _spender Spender address
    /// @param _value Value
    function approveToken(
        Token _tokenContract,
        address _spender,
        uint256 _value
    ) onlyOwner external {
        _tokenContract.approve(_spender, _value);
    }

    /// @notice Relay a signed instruction from a party of an escrow.
    /// @param _tradeID Escrow "tradeID" parameter
    /// @param _seller Escrow "seller" parameter
    /// @param _buyer Escrow "buyer" parameter
    /// @param _value Escrow "value" parameter
    /// @param _fee Escrow "fee parameter
    /// @param _maximumGasPrice Maximum gas price permitted for the relayer (set by the instructor)
    /// @param _v Signature "v" component
    /// @param _r Signature "r" component
    /// @param _s Signature "s" component
    /// @param _additionalGas Additional gas to be deducted after this operation
    /// @return bool
    function relay(
        bytes16 _tradeID,
        address payable _seller,
        address payable _buyer,
        uint256 _value,
        uint16 _fee,
        uint128 _maximumGasPrice,
        uint8 _v,
        bytes32 _r,
        bytes32 _s,
        uint8 _instructionByte,
        uint128 _additionalGas
    ) private returns (bool) {
        address _relayedSender = getRelayedSender(
            _tradeID,
            _instructionByte,
            _maximumGasPrice,
            _v,
            _r,
            _s
        );
        if (_relayedSender == _buyer) {
            // Buyer's instructions:
            if (_instructionByte == INSTRUCTION_SELLER_CANNOT_CANCEL) {
                // Disable seller from cancelling
                return doDisableSellerCancel(_tradeID, _seller, _buyer, _value, _fee, _additionalGas);
            } else if (_instructionByte == INSTRUCTION_BUYER_CANCEL) {
                // Cancel
                return doBuyerCancel(_tradeID, _seller, _buyer, _value, _fee, _additionalGas);
            }
        } else if (_relayedSender == _seller) {
            // Seller's instructions:
            if (_instructionByte == INSTRUCTION_RELEASE) {
                // Release
                return doRelease(_tradeID, _seller, _buyer, _value, _fee, _additionalGas);
            } else if (_instructionByte == INSTRUCTION_SELLER_CANCEL) {
                // Cancel
                return doSellerCancel(_tradeID, _seller, _buyer, _value, _fee, _additionalGas);
            } else if (_instructionByte == INSTRUCTION_SELLER_REQUEST_CANCEL) {
                // Request to cancel
                return doSellerRequestCancel(_tradeID, _seller, _buyer, _value, _fee, _additionalGas);
            }
        }
        return false;
    }

    /// @notice Increase the amount of gas to be charged later on completion of an escrow
    /// @param _tradeHash Trade hash
    /// @param _gas Gas cost
    function increaseGasSpent(bytes32 _tradeHash, uint128 _gas) private {
        escrows[_tradeHash].totalGasFeesSpentByRelayer += _gas * uint128(tx.gasprice);
    }

    /// @notice Transfer the value of an escrow, minus the fees, minus the gas costs incurred by relay
    /// @param _to Recipient address
    /// @param _value Value of the transfer
    /// @param _totalGasFeesSpentByRelayer Total gas fees spent by the relayer
    /// @param _fee Commission in 1/10000ths
    function transferMinusFees(
        address payable _to,
        uint256 _value,
        uint128 _totalGasFeesSpentByRelayer,
        uint16 _fee
    ) private {
        uint256 _totalFees = (_value * _fee / 10000) + _totalGasFeesSpentByRelayer;
        // Prevent underflow
        if(_value - _totalFees > _value) {
            return;
        }
        // Add fees to the pot for localethereum to withdraw
        feesAvailableForWithdraw += _totalFees;
        payable(_to).transfer(_value - _totalFees);
    }

    uint16 constant GAS_doRelease = 12664;
    /// @notice Release escrow to the buyer. This completes it and removes it from the mapping.
    /// @param _tradeID Escrow "tradeID" parameter
    /// @param _seller Escrow "seller" parameter
    /// @param _buyer Escrow "buyer" parameter
    /// @param _value Escrow "value" parameter
    /// @param _fee Escrow "fee parameter
    /// @param _additionalGas Additional gas to be deducted after this operation
    /// @return bool
    function doRelease(
        bytes16 _tradeID,
        address _seller,
        address payable _buyer,
        uint256 _value,
        uint16 _fee,
        uint128 _additionalGas
    ) private returns (bool) {
        Escrow memory _escrow;
        bytes32 _tradeHash;
        (_escrow, _tradeHash) = getEscrowAndHash(_tradeID, _seller, _buyer, _value, _fee);
        if (!_escrow.exists) return false;
        uint128 _gasFees = _escrow.totalGasFeesSpentByRelayer
            + (relayers[msg.sender] == true
                ? (GAS_doRelease + _additionalGas ) * uint128(tx.gasprice)
                : 0
            );
        delete escrows[_tradeHash];
        emit Released(_tradeHash);
        transferMinusFees(_buyer, _value, _gasFees, _fee);
        return true;
    }

    uint16 constant GAS_doDisableSellerCancel = 16568;
    /// @notice Prevents the seller from cancelling an escrow. Used to "mark as paid" by the buyer.
    /// @param _tradeID Escrow "tradeID" parameter
    /// @param _seller Escrow "seller" parameter
    /// @param _buyer Escrow "buyer" parameter
    /// @param _value Escrow "value" parameter
    /// @param _fee Escrow "fee parameter
    /// @param _additionalGas Additional gas to be deducted after this operation
    /// @return bool
    function doDisableSellerCancel(
        bytes16 _tradeID,
        address _seller,
        address _buyer,
        uint256 _value,
        uint16 _fee,
        uint128 _additionalGas
    ) private returns (bool) {
        Escrow memory _escrow;
        bytes32 _tradeHash;
        (_escrow, _tradeHash) = getEscrowAndHash(_tradeID, _seller, _buyer, _value, _fee);
        if (!_escrow.exists) return false;
        if(_escrow.sellerCanCancelAfter == 0) return false;
        escrows[_tradeHash].sellerCanCancelAfter = 0;
        emit SellerCancelDisabled(_tradeHash);
        if (relayers[msg.sender] == true) {
          increaseGasSpent(_tradeHash, GAS_doDisableSellerCancel + _additionalGas);
        }
        return true;
    }

    uint16 constant GAS_doBuyerCancel = 12648;
    /// @notice Cancels the trade and returns the ETH to the seller. Can only be called the buyer.
    /// @param _tradeID Escrow "tradeID" parameter
    /// @param _seller Escrow "seller" parameter
    /// @param _buyer Escrow "buyer" parameter
    /// @param _value Escrow "value" parameter
    /// @param _fee Escrow "fee parameter
    /// @param _additionalGas Additional gas to be deducted after this operation
    /// @return bool
    function doBuyerCancel(
        bytes16 _tradeID,
        address payable _seller,
        address _buyer,
        uint256 _value,
        uint16 _fee,
        uint128 _additionalGas
    ) private returns (bool) {
        Escrow memory _escrow;
        bytes32 _tradeHash;
        (_escrow, _tradeHash) = getEscrowAndHash(_tradeID, _seller, _buyer, _value, _fee);
        if (!_escrow.exists) {
            return false;
        }
        uint128 _gasFees = _escrow.totalGasFeesSpentByRelayer
            + (relayers[msg.sender] == true
                ? (GAS_doBuyerCancel + _additionalGas ) * uint128(tx.gasprice)
                : 0
            );
        delete escrows[_tradeHash];
        emit CancelledByBuyer(_tradeHash);
        transferMinusFees(_seller, _value, _gasFees, 0);
        return true;
    }

    uint16 constant GAS_doSellerCancel = 13714;
    /// @notice Returns the ETH in escrow to the seller. Called by the seller. Sometimes unavailable.
    /// @param _tradeID Escrow "tradeID" parameter
    /// @param _seller Escrow "seller" parameter
    /// @param _buyer Escrow "buyer" parameter
    /// @param _value Escrow "value" parameter
    /// @param _fee Escrow "fee parameter
    /// @param _additionalGas Additional gas to be deducted after this operation
    /// @return bool
    function doSellerCancel(
        bytes16 _tradeID,
        address payable _seller,
        address _buyer,
        uint256 _value,
        uint16 _fee,
        uint128 _additionalGas
    ) private returns (bool) {
        Escrow memory _escrow;
        bytes32 _tradeHash;
        (_escrow, _tradeHash) = getEscrowAndHash(_tradeID, _seller, _buyer, _value, _fee);
        if (!_escrow.exists) {
            return false;
        }
        if(_escrow.sellerCanCancelAfter <= 1 || _escrow.sellerCanCancelAfter > block.timestamp) {
            return false;
        }
        if (relayers[msg.sender] == false && _escrow.sellerCanCancelAfter + 12 hours > block.timestamp) {
            return false;
        }
        uint128 _gasFees = _escrow.totalGasFeesSpentByRelayer
            + (relayers[msg.sender] == true
                ? (GAS_doSellerCancel + _additionalGas ) * uint128(tx.gasprice)
                : 0
            );
        delete escrows[_tradeHash];
        emit CancelledBySeller(_tradeHash);
        transferMinusFees(_seller, _value, _gasFees, 0);
        return true;
    }

    uint16 constant GAS_doSellerRequestCancel = 17004;
    /// @notice Request to cancel. Used if the buyer is unresponsive. Begins a countdown timer.
    /// @param _tradeID Escrow "tradeID" parameter
    /// @param _seller Escrow "seller" parameter
    /// @param _buyer Escrow "buyer" parameter
    /// @param _value Escrow "value" parameter
    /// @param _fee Escrow "fee parameter
    /// @param _additionalGas Additional gas to be deducted after this operation
    /// @return bool
    function doSellerRequestCancel(
        bytes16 _tradeID,
        address _seller,
        address _buyer,
        uint256 _value,
        uint16 _fee,
        uint128 _additionalGas
    ) private returns (bool) {
        // Called on unlimited payment window trades where the buyer is not responding
        Escrow memory _escrow;
        bytes32 _tradeHash;
        (_escrow, _tradeHash) = getEscrowAndHash(_tradeID, _seller, _buyer, _value, _fee);
        if (!_escrow.exists) {
            return false;
        }
        if(_escrow.sellerCanCancelAfter != 1) {
            return false;
        }
        escrows[_tradeHash].sellerCanCancelAfter = uint32(block.timestamp)
            + requestCancellationMinimumTime;
        emit SellerRequestedCancel(_tradeHash);
        if (relayers[msg.sender] == true) {
          increaseGasSpent(_tradeHash, GAS_doSellerRequestCancel + _additionalGas);
        }
        return true;
    }

    /// @notice Get the sender of the signed instruction.
    /// @param _tradeID Identifier of the trade
    /// @param _instructionByte Identifier of the instruction
    /// @param _maximumGasPrice Maximum gas price permitted by the sender
    /// @param _v Signature "v" component
    /// @param _r Signature "r" component
    /// @param _s Signature "s" component
    /// @return address
    function getRelayedSender(
      bytes16 _tradeID,
      uint8 _instructionByte,
      uint128 _maximumGasPrice,
      uint8 _v,
      bytes32 _r,
      bytes32 _s
    ) view private returns (address) {
        bytes32 _hash = keccak256(abi.encodePacked(
            _tradeID,
            _instructionByte,
            _maximumGasPrice
        ));
        if(tx.gasprice > _maximumGasPrice) {
            return (address)(0);
        }
        return recoverAddress(_hash, _v, _r, _s);
    }

    /// @notice Hashes the values and returns the matching escrow object and trade hash.
    /// @dev Returns an empty escrow struct and 0 _tradeHash if not found.
    /// @param _tradeID Escrow "tradeID" parameter
    /// @param _seller Escrow "seller" parameter
    /// @param _buyer Escrow "buyer" parameter
    /// @param _value Escrow "value" parameter
    /// @param _fee Escrow "fee parameter
    /// @return Escrow
    function getEscrowAndHash(
        bytes16 _tradeID,
        address _seller,
        address _buyer,
        uint256 _value,
        uint16 _fee
    ) view private returns (Escrow memory, bytes32) {
        bytes32 _tradeHash = keccak256(abi.encodePacked(
            _tradeID,
            _seller,
            _buyer,
            _value,
            _fee
        ));
        return (escrows[_tradeHash], _tradeHash);
    }

    /// @notice Returns an empty escrow struct and 0 _tradeHash if not found.
    /// @param _h Data to be hashed
    /// @param _v Signature "v" component
    /// @param _r Signature "r" component
    /// @param _s Signature "s" component
    /// @return address
    function recoverAddress(
        bytes32 _h,
        uint8 _v,
        bytes32 _r,
        bytes32 _s
    ) private pure returns (address) {
        bytes memory _prefix = "\x19Ethereum Signed Message:\n32";
        bytes32 _prefixedHash = keccak256(abi.encodePacked(_prefix, _h));
        return ecrecover(_prefixedHash, _v, _r, _s);
    }
}
Click to expand…

Using LocalCryptos, an ordinary trade works like this:

  1. The buyer and seller confirm and agree on the terms of the trade.
  2. The seller places the ETH into the smart contract. This provides proof-of-funds and allows for a much safer trade.
  3. The buyer makes payment directly to the seller.
  4. Either:
    1. The seller successfully confirms the payment, and releases the escrow. Trade complete!
    2. A party raises a dispute, and brings in a third-party arbitrator, giving them the keys to decrypt the messages and work with both parties to make a resolution.

The smart contract allows users to safely exchange ETH with one another, and to name a trusted third-party to mediate a trade if a dispute arises.

Creating and funding an escrow

Technically, escrows are not directly linked to trades on LocalCryptos, and, if we allowed it, the contract could be utilized to escrow transactions outside of LocalCryptos (perhaps for other real-world goods and services). When you first open a trade with somebody using LocalCryptos, no escrow exists on the blockchain... until the seller initiates and funds it in a single transaction.

Every escrow first requires a signed invitation from marketplace, which has the superfluous purpose of keeping the contract clean of spam and accidental transfers. The seller can request one of these signatures from LocalCryptos's API when they're ready to place their ETH in escrow. The temporary invitation contains a signature of the trade's properties, including:

  1. The buyer's address used to interact with the escrow and receive funds
  2. The seller's address used to interact with the escrow, and receive returned ETH in case of a cancellation
  3. The size of the trade in ETH
  4. The marketplace's fee percentage
  5. The payment window in seconds (except for cash trades)

Creating an escrow requires making a call to the external createEscrow function with these parameters and the signed invitation. The function can be called from any address.

Once the initial createEscrow transaction has been confirmed by the network, a verifiable decentralized escrow account exists on the blockchain.

Avoiding up-front miner fees with a relay system

Peer-to-peer marketplaces are many people's first interaction with Ethereum and cryptocurrencies in general, which complicates things. It costs ETH to interact with the blockchain in terms of gas, and somebody who doesn't yet have any ETH is unable to interact with smart contracts directly.

With this in mind, we designed a system in which traders could interact the smart contract for "free" using a proxy. The cost of gas was paid up-front by us to relay digital signatures which authorised instructions on a user's behalf, with the expectation that we'd be reimbursed at the end of the escrow.

At first glance this system may sound exploitable. What's to stop users from racking up debt with no intention to pay? There are a few reasons why fronting gas costs is not very gameable:

  1. The marketplace chose the gas price, and kept it reasonable.
  2. It only relayed what is legitimate and important.
  3. There's a strong financial disincentive to an attack of this nature, because it would involve the attacker burning lots of ETH on gas and fees.
  4. The marketplace's API is rate-limited and protected by CAPTCHAs plus other bot-deterrents, and you needed signed permission from the API to create an escrow.
  5. The overhead on relaying actions was reduced by sending many at a time, modifying many escrows in a single transaction. (As escrow activity increases, the transaction overhead costs will continue to shrink in proportion.)

Making changes to the escrow

During the course of an escrow, there are a few actions each party can take. Ordinarily, these actions are performed via relay, but the parties have the choice to make calls externally if they prefer. Each "action" has a single-byte identifier, which are defined as constants near the beginning of the smart contract's source code.

To invoke an action via relay, the caller simply needs to sign a Keccak hash of the concatenation of the trade ID, the action byte and a maximum gas price that the relayer is allowed to spend for the action. (The gas price cap prevents a theoretical attack vector whereby the relayer could overpay for gas and cause the parties to burn more than a reasonable amount on network fees.)

Releasing funds (action 0x05).The seller can release the funds to the buyer at any time during the escrow. This will end the escrow, and its balance, minus network fees and the marketplace's fee, will be sent to the buyer's address.

Cancelling as a buyer (action 0x02).The buyer can cancel the escrow at any time, returning the funds to the seller. The marketplace fee won't be deducted from the balance, but any unpaid network fees covered by the relayer will be taken from the total.

Preventing the seller from cancelling (action 0x01).The buyer can halt the payment window countdown and prevent the seller from being able to cancel the escrow. This is done to indicate that the buyer has made payment, and he expects the ETH to be released soon.

Once the escrow has entered this stage where the buyer has essentially locked the ETH in escrow, there are three ways the escrow can end:

  1. The seller can release the funds.
  2. The buyer can cancel the trade.
  3. Either party can call in the arbitrator to resolve the escrow.

When a buyer asks the relayer to send this action to the blockchain, the relayer doesn't do so immediately. In fact, in most successful escrows, this action is never broadcasted to the network. This is because it's only important to relay this before the payment window has expired, and there's no benefit to doing it much earlier. The servers held on to the relay request and only sent it roughly twenty minutes before the payment window was due to expire (which is more than enough time for the transaction to safely propagate and confirm). In most successful escrows, the seller has already released the escrow by this time, and so this action becomes redundant.

By not relaying unnecessary actions immediately, this allows the traders to save on network fees. (Most successful trades invoke only createEscrow and the seller's release action.)

Cancelling as a seller (action 0x03).The seller cannot as easily cancel as the buyer, and can only do so if the buyer allows it. The seller first needs to wait until the payment window has expired before they can withdraw their funds from escrow.

Escrow identifiers

Allocating storage in an Ethereum smart contract can be really expensive if you're not careful. Of all of the operations available in the EVM, writing to fresh storage is by far the most costly operation.

The LocalCryptos Solidity smart contract is designed to keep a minimal storage footprint. Each escrow in the contract requires an allocation of only two 256-bit words, or 64 bytes in total. This is accomplished by tightly packing static properties of the trade into a "trade hash", and then using that hash as the unique key to the escrow in the public mapping. By packing these into the hash, we avoid the need for allocating extra space for each static property.

Escrows are stored in a public variable named escrows which is a mapping of the custom Escrow struct:

mapping (bytes32 => Escrow) public escrows;

The 32-byte keys to the mapping are Keccak hash functions of the tightly-packed concatenation of:

  • Trade ID (16 bytes): Although always the related UUID identifier of a trade on LocalCryptos, any unique nonce would work just fine in its place. This is to make sure that no two trades ever collide.
  • Seller address (24 bytes): The address of the seller. (This does't need to be the same address that the funds are deposited from — it can be freshly generated.)
  • Buyer address (24 bytes): The address of the buyer.
  • Value (256 bytes, unsigned): The value of the trade (in wei).
  • Fee (16 bytes, unsigned): LocalCryptos's commission of the trade upon successful release, represented in 1/10,000ths.

Once we have all of the static properties of the trade, we can generate the trade hash with a simple Keccak-256 function in Solidity.

The Escrow struct

Information about each escrow is stored in an Escrow struct. This struct fits an allocation of one word; although only 161 bits of storage are needed, the EVM allocates storage only in 256-bit blocks.

struct Escrow {
  bool exists;
  uint32 sellerCanCancelAfter;
  uint128 totalGasFeesSpentByRelayer;
}

The struct contains these variables:

  • exists: This is simply a boolean to indicate that the escrow exists, since there is no way to differentiate unused storage from zero in Solidity. This is always true until the escrow is released or cancelled.
  • sellerCanCancelAfter: This is initially an epoch timestamp containing the date after which a seller is permitted to cancel the escrow and return the funds to themselves. There are two other special values:
    • A value of 0 indicates that the seller is not allowed to cancel at any time. The value can be set to 0 at any time at the buyer's request to lock the funds in the escrow until the seller releases the funds or the arbitrator steps in to resolve the trade. This is typically done when buyer indicates he has made payment, and expects the funds to be released.
    • For specific payment types where payment windows don't make as much sense (e.g. cash) this is set to 1, which is a special value to mean infinity. When sellerCanCancelAfter equals 1, the seller can't cancel, but can initiate a request to cancel. When the seller indicates that he'd like to cancel the trade, the buyer is given adequate time to dispute the request before the seller-cancellation is permitted.
  • totalGasFeesSpentByRelayer: This is a counter of the gas accumulated by relay operations. It will be deducted from the escrow to cover network fees once it is released or cancelled.

Disputing a trade

In the case of a dispute, either party needs to give the arbitrator their signed dispute token. With a signature of this dispute token, which is simply the output of sha3(Trade ID, 0x06), the arbitrator can resolve the dispute in either party's favour.

To do this, we call the external resolveDispute function with the trade's static properties, the signed dispute token, and the percentage of the escrow's balance that will be sent to each party. It is impossible for the arbitrator to have the disputed ETH sent to anybody else.

More details

In addition to this page, which collates most of the information related to LocalCryptos' inner-workings, you can learn more about LocalCryptos in our Academy section or FAQ. You can also find information in our security page.

If you have any questions, please reach out to us.