Last modified: Fri Sep 20 2019 11:21:30 GMT+0000 (Coordinated Universal Time)

Building Trust

Trust is the cornerstone of any network, especially one that involves money. Currently, the usual way of evaluating whether a certain company is trustworthy enough to do business with others is decided by market makers, like big OTAs and GDSs. The processes they have are slow, inefficient and costly. Winding Tree provides an alternative system for building trust.

First of all, you should always check that the orgJsonHash presented by the ORG.ID you want to work with matches the data on orgJsonUri. It should just be a matter of generating a keccak256 equivalent hash by yourself and comparing it to the orgJsonHash value. If the hashes don't match, an unauthorized change happened and it is possible that the Organization has been compromised.

Trust Clues

A trust clue is a costly signal, a piece of information about a particular ORG.ID that undeniably and unambiguously establishes a certain fact about the identity it's connected to. These principles are behind the system of trust clues on Winding Platform:

  1. It should be easy (cheap) to send out an honest signal.
  2. It should be very hard (expensive) to cheat (send out bad signals or clues).
  3. It should be easy to reveal dishonest behavior.
  4. Dishonest players will be facing severe consequences in the form of tarnished reputation that other market players should be notified of immediately.

Info

The list of supported and implemented trust clues will grow. You can learn about some ideas here.

Every ORG.ID should strive for the greatest possible trust level they can achieve, which can be done by providing multiple trust clues (and refraining from dishonest behavior). On the other hand, other participants in the system can pick with which companies they will cooperate by evaluating their trust level.

In this tutorial, we will use the Líf Deposit as an example clue. Other ideas are out there, but we have not yet implemented any of those.

Líf Deposit

Líf Deposit is one of the most important clues that signals that the ORG.ID was not created by a spammer. Creating ORG.IDs is cheap, therefore a malicious actor can generate hundreds of ORG.IDs for fraudulent purposes, but an honest player may submit a 1000 Líf deposit to a special smart contract to prove their goodwill, which the fraudster won't be able to do (it would be too expensive). The ORG.ID owner may request to return their deposit at any time, but this would automatically invalidate their deposit, and the refund will be executed with a certain delay.

1. Obtaining Líf

We will be using Ropsten testnet in this guide, so you won't have to spend real money while testing the platform. So go ahead and get Líf from the faucet we created for testing purposes.

On mainnet, you can get LIF on an exchange such as IDEX, there is no faucet. Make sure to use the same network for both ORG.ID setup and LIF deposit.

We continue to use the truffle console we started in the ORG.ID tutorial.

truffle(ropsten)> lif = await LifTokenTest.at('0xB6e225194a1C892770c43D4B529841C99b3DA1d7') // Returns `undefined`
truffle(ropsten)> lif.faucetLif() // Wait for transaction to succeed

To create an Org ID on Mainnet you will be using real Lífs. Since LifToken is an ERC20 token, we can use its interface. There is no faucet available unfortunately.

truffle(mainnet)> lif = await IERC20.at('0xEB9951021698B42e4399f9cBb6267Aa35F82D59D') // Returns `undefined`

The faucetLif() above should have put 50 Lífs into the caller's account (the default account that the truffle console is using). Let's check if it worked:

// The `balanceOf` returns an instance of Big Number, so we have to call the toString method on it
truffle(ropsten)> balance = await lif.balanceOf('0xd054c6df3314a1b49f2b7ec0bf1011405a5efdff')
// You should see '50000000000000000000'
truffle(ropsten)> balance.toString()

The balance is returned in 10^-18 denomination (LífWei, if you will), so instead of 50 you see 50000000000000000000, which is correct. You can also check your Líf balance on Etherscan.

2. Depositing Líf

Let's deposit 25 Lífs to the Líf Deposit smart contract on Ropsten (0xfCfD5E296E4eD50B5F261b11818c50B73ED6c89E) on behalf of the ORG.ID we created earlier.

Note that only accounts associated with the ORG.ID can deposit on its behalf. Adding an associated key has to be done by the 0xORG owner.

truffle(ropsten)> orgid.addAssociatedKey('0xd054c6df3314a1b49f2b7ec0bf1011405a5efdff')
{ tx: '0xabc...',
  ...
}
// now we can check it was really added
truffle(ropsten)> orgid.getAssociatedKeys()
[ '0x0000000000000000000000000000000000000000',
  '0xd054c6df3314a1b49f2b7ec0bf1011405a5efdff' ]

Next, we have to approve the transfer on the token itself and after that we can do the actual deposit. This is a feature of all ERC20 tokens. We have been working on another approach, that would allow us not to have to do the explicit approve. If you are interested in the progress, look up ERC827 specification.

// returns `undefined`, as usual
truffle(ropsten)> lifdeposit = await LifDeposit.at('0xfCfD5E296E4eD50B5F261b11818c50B73ED6c89E')
// We have to approve tokens to withdraw manually, wait for transaction to succeed
truffle(ropsten)> lif.approve(lifdeposit.address, '25000000000000000000')
// Should return a successful transaction object
truffle(ropsten)> lifdeposit.addDeposit(orgid.address, '25000000000000000000')

You can check your Líf balance now to see that you only have 25 Lífs and that the Líf Deposit contract is 25 Lífs richer.

3. Checking Deposit

If we want to check a deposit, we would just ask for a deposit value for given 0xORG.

truffle(ropsten)> depositValue = await lifdeposit.getDepositValue(orgid.address)
truffle(ropsten)> depositValue.toString() // returns '25000000000000000000'

4. Withdrawing Líf

Withdrawing is a two step process. First, you have to ask for a return which decreases the reported deposited value for the organization. After a configurable amount of time passes, you are able to actually withdraw the funds and have them transferred to the caller's account.

Same as depositing, both actions have to be called by one of the associated keys (accounts). Note that there may be more associated keys and it may change in time. That means depositing, asking for withdrawal and withdrawing can be performed by different accounts. LifDeposit only checks if the caller's account is among associated keys at the moment of calling each respective method.

truffle(ropsten)> lifdeposit.askForDepositWithdrawal(orgid.address, '25000000000000000000') // ask for my 25 lif back
// ... some time passes (by default about 1 month)
truffle(ropsten)> lifdeposit.withdrawDeposit(orgid.address, '25000000000000000000')

Appointing a Guarantor

Within a network of ORG.IDs it is possible that one organization (with high level of trust) can guarantee that another ORG.ID is trustworthy. It could be useful in franchising where franchisor guarantees that their franchisees are legitimate businesses in good standing.

It is based on publishing a signed claim that reuses technology that's already in the Ethereum techstack.

Info

A signing scheme such as this is very generic and can be used in other places, such as in Direct Connect or as a generic way of disclosing any kind of relationship between two Ethereum addresses. It does not have to be tied to trust evaluation.

1. Creating a Claim

The claim can be arbitrary. However, to ensure that everything works on various transport channels with various limitations, we should use a pattern where the message always contains at least:

  • The guaranteeing ORG.ID's Ethereum address (guarantor)
  • The target ORG.ID's Ethereum address (subject)
  • An expiration date (expiresAt)

So this is how a guarantee relationship claim might look like:

{
  "subject": "0x6022a8191c25382d93e4957e7e283342f5dcb9ca",
  "guarantor": "0x23DC34b37d84A159417Cd6c4039dc0CE73C36F2a",
  "expiresAt": 1561043452
}

2. Signing a Claim

Since we are using Ethereum private-public key pairs, we can benefit from the integrated message signing algorithm that uses a built in eth_sign method. With that comes an opposite method usually called ecrecover or recover.

The eth_sign accepts the message and a private key and produces a signature, and the ecrecover accepts the message and signature and produces the public key fingerprint (Ethereum account address) that was used to sign the message.

Warning

As any cryptographic feature, signatures are very sensitive to content changes. It is vital that the claim is passed to the other party in the exact same way in which it was signed. So be aware if you are using a different implementation of web3 library. The eth_sign method also has some specifics (message prefixing) to it. That is why we send the claim hex-encoded instead of raw JSON.

The resulting claim-signature pair is then published in the ORG.JSON data file:

{
  ...
  "guarantee": {
    "claim": "hex-encoded JSON",
    "signature": "signature from an eth_sign-compatible method"
  }
}

In JavaScript or Node environment we can use the Web3.js library to sign claims. We have a create-signed-message.js script in the tutorial repository.

Info

You can also use our guarantee generator tool.

const web3 = require('web3-eth-accounts');
const web3utils = require('web3-utils');
const utils = require('./utils');

const keyFile = require('./keys.json');
const provider = utils.getInfuraNodeAddress('ropsten', keyFile.infura_projectid);
const account = utils.getWeb3Account(provider, keyFile);

console.info(`Using account ${account.address} for signing`);

const claim = {
  subject: '0x6022a8191c25382d93e4957e7e283342f5dcb9ca', // my org.id address
  guarantor: '0x6022a8191c25382d93e4957e7e283342f5dcb9ca', // my org.id address
  expiresAt: 1561043452
};
// because publishThis is JSON as well, we are hex encoding the claim to prevent some parsers from eager interpretation
const encodedClaim = web3utils.utf8ToHex(JSON.stringify(claim));
const signed = account.sign(encodedClaim);
const publishThis = {
  claim: encodedClaim,
  signature: signed.signature
};
console.log(publishThis);
process.exit(0);

Warning

The messages are not encrypted, only encoded. Anybody who intercepts the traffic can read the contents. You should always use secure encrypted channels such as HTTPS to prevent traffic eavesdropping.

3. Verifying a Guarantee

If you encounter a guarantee like this, you have to do the following to verify its validity. We have a verify-signed-message.js script in the tutorial repository.

3.1 Decode and verify a Claim

const web3 = require('web3-eth-accounts');
const web3utils = require('web3-utils');
const utils = require('./utils');

const keyFile = require('./keys.json');
const provider = utils.getInfuraNodeAddress('ropsten', keyFile.infura_projectid);
const accounts = web3.Accounts(provider);

const receivedThis = {
  "claim": "0x7b227375626a656374223a22307836303232613831393163323533383264393365343935376537653238333334326635646362396361222c2267756172616e746f72223a22307836303232613831393163323533383264393365343935376537653238333334326635646362396361222c22657870697265734174223a313536343734383033377d",
  "signature": "0x8bfbe2849552bf959c7bf332dbff3cc975964dc4d33bb842eddf365238ad83e672ae65b61073bbf88259c7287c7fe51246ac4e840e137ee338234ffb053bf8531b"
};

// because publishThis is JSON as well, we are hex encoding the claim to prevent some parsers from eager interpretation
const actualSigner = accounts.recover(receivedThis.claim, receivedThis.signature);
const decodedClaim = JSON.parse(web3utils.hexToUtf8(receivedThis.claim));
console.log({
  signedBy: actualSigner,
  subject: decodedClaim.subject,
  guarantor: decodedClaim.guarantor,
  expiresAt: decodedClaim.expiresAt,
});
process.exit(0);

If the recover method fails, you know that the claim or signature has been tampered with during transport.

3.2. Evaluate Claim Validity

Once the verifying side knows that the message was legitly signed and was not tampered with, it can proceed to the next steps.

  1. Check that the guarantee has not expired by comparing expiresAt to the current date and time.
  2. Check if the signer is actually associated with the guaranteeing ORG.ID by calling hasAssociatedKey on 0xORG smart contract - that's an important step that establishes the actual validity of the guarantee. This can be done in truffle console.
     truffle(ropsten)> org = await Organization.at('decodedClaim.guarantor from the previous example')
     undefined
     truffle(ropsten)> org.hasAssociatedKey('signedBy from the previous example')
     true
    
  3. Check if the guarantor can be trusted. In case of our Líf Deposit we can check in truffle console
     truffle(ropsten)> lifdeposit = await LifDeposit.at('0xfCfD5E296E4eD50B5F261b11818c50B73ED6c89E')
     undefined
     truffle(ropsten)> depositValue = await lifdeposit.getDepositValue('decodedClaim.guarantor from the previous example')
     truffle(ropsten)> depositValue.toString()
    

A similar signing and signature verification scheme can be used in any kind of communication between ORG.ID as we describe in the Direct Connect part of this tutorial.