SNT Gas Relay
Go to file
Richard Ramos 3347c4fc68 Merge branch 'master' of https://github.com/status-im/snt-gas-relay 2018-09-03 09:32:19 -04:00
gas-relayer Updated approveAndCall section 2018-09-01 07:47:43 -04:00
test-dapp Updating library to include snt controller functions 2018-09-01 16:35:56 -04:00
.gitattributes cleanup bootstrap 2018-04-23 01:58:58 -03:00
.gitignore add package-lock to.json .gitignore 2018-05-13 12:41:38 -03:00
README.md Updated documentation 2018-09-03 09:31:37 -04:00

README.md

token-gas-relayer

Gas relayer mplementation for economic abstraction. This project consists of two elements:

  • gas-relayer: nodejs service that listens to whisper on a symmetric key, with specific topics, and processes any transaction.
  • test-dapp: DApp created for testing purposes. It allows the easy creation of the messages expected by the service.

Installation

  • geth is required for installation
  • Install the latest develop version of embark: npm install -g https://github.com/embark-framework/embark.git
  • If running a development version of the gas relay
cd test-dapp
chmod a+x setup_dev_env.sh
npm install
embark reset
embark blockchain
  • When Embark finishes loading, execute embark run && ./setup_dev_env.sh to create the test account

Node

Before executing this program, config/config.json must be setup and npm install needs to be executed. Important values to verify are related to the node configuration, just like:

  • Host, port and protocol to connect to the geth node
  • Host, port and protocol Ganache will use when forking the blockchain for gas estimations and other operations
  • Wallet account used for processing the transactions
  • Symmetric key used to receive the Whisper messages
  • Accepted tokens information
  • Contract configuration

This program is configured with the default values for a embark installation run from 0

A geth node running whisper (via -shh option) is required. To execute the gas-relayer, you may use any of the following three methods.

npm start
node src/service.js
nodemon src/service.js

Test DApp

To run the test dapp, use embark run and then browse http://localhost:8000/index.html.

The gas relayer service needs to be running, and configured correctly to process the transactions. Things to take in account are: the account used in embark, and the contract addresses.

(TODO: update testnet configuration to guarantee the contract addresses don't change)

The relayer

A node that wants to act as a relayer only needs to have a geth node with whisper enabled, and an account with ether to process the transactions. This account and node need to be configured in ./config/config.js.

The relayer will be subscribed to receive messages in a specific symkey (this will change in the future to use ENS), and will reply back to both availability and transaction requests

Using the gas relayer

The protocol

Sending a message to the gas relayer network (all accounts and privatekeys should be replaced by your own test data)

shh.post({
    symKeyID: SYM_KEY,  // If requesting availability
    pubKey: PUBLIC_KEY_ID,  // If sending a transaction
    sig: WHISPER_KEY_ID, 
    ttl: 1000, 
    powTarget: 1, 
    powTime: 20, 
    topic: TOPIC_NAME, 
    payload: PAYLOAD_BYTES
}).then(......)
  • symKeyID: SYM_KEY must contain the whisper symmetric key used. It is shown on the console when running the service with node. With the provided configuration you can use the symmetric key 0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b. Only used when asking for relayer availability.
  • pubKey: PUBLIC_KEY_ID. After asking for availability, once the user decides on a relayer, it needs to set the pubKey attribute with the relayer public key (received in the availability reply in the sig attribute of the message).
  • WHISPER_KEY_ID represents a keypair registered on your node, that will be used to sign the message. Can be generated with web3W.shh.newKeyPair()
  • TOPIC_NAME must contain one of the topic names generated based on converting the contract name to hex, and taking the first 8 bytes. For the provided configuration the following topics are available:
    • IdentityGasRelay: 0x4964656e
    • SNTController: 0x534e5443
  • PAYLOAD_BYTES a hex string that contains details on the operation to perform.

Polling for gas relayers

The first step is asking the relayers for their availability. The message payload needs to be the hex string representation of a JSON object with a specific structure:

const payload = web3.utils.toHex({
        'contract': "0xContractToInvoke",
        'address': web3.eth.defaultAccount,
        'action': 'availability',
        'token': "0xGasTokenAddress",
        'gasPrice': 1234
    });
  • contract is the address of the contract that will perform the operation, in this case it can be an Identity, or the SNTController.
  • address The address that will sign the transactions. Normally it's web3.eth.defaultAccount
  • gasToken: token used for paying the gas cost
  • gasPrice: The gas price used for the transaction

This is a example code of how to send an 'availability' message:

const whisperKeyPairID = await web3W.shh.newKeyPair();

const msgObj = { 
    symKeyId: "0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b", 
    sig: whisperKeyPairID,
    ttl: 1000, 
    powTarget: 1, 
    powTime: 20, 
    topic: "0x4964656e", 
    payload: web3.utils.toHex({
        'contract': "0x692a70d2e424a56d2c6c27aa97d1a86395877b3a",
        'address': web3.eth.defaultAccount
        'action': 'availability',
        'gasToken': "0x744d70fdbe2ba4cf95131626614a1763df805b9e",
        'gasPrice': 40000000000 // 40 gwei equivalent in SNT
    })
};

        
web3.shh.post(msgObj)
.then((err, result) => {
    console.log(result);
    console.log(err);
});

When it replies, you need to extract the sig attribute to obtain the relayer's public key

Sending transaction details

Sending a transaction is similar to the previous operation, except that we send the message to an specific node, we use the action transaction, and also we send a encodedFunctionCall with the details of the transaction to execute.

From the list of relayers received via whisper messages, you need to extract the message.sig to obtain the pubKey. This value is used to send the transaction to that specific relayer.

encodedFunCall is the hex data used obtained from web3.eth.abi.encodeFunctionCall for the specific function we want to invoke.

If we were to execute callGasRelayed(address,uint256,bytes,uint256,uint256,uint256,address,bytes) (part of the IdentityGasRelay) in contract 0x692a70d2e424a56d2c6c27aa97d1a86395877b3a, with these values: "0x11223344556677889900998877665544332211",100,"0x00",1,10,20,"0x1122334455112233445511223344551122334455", "0x1122334455", PAYLOAD_BYTES can be prepared as follows:

// The following values are created obtained when polling for relayers
const whisperKeyPairID = await web3W.shh.newKeyPair();
const relayerPubKey = "0xRELAYER_PUBLIC_KEY_HERE";
// ...
// ...
const jsonAbi = ABIOfIdentityGasRelay.find(x => x.name == "callGasRelayed");

const funCall = web3.eth.abi.encodeFunctionCall(jsonAbi,
                [
                    "0x11223344556677889900998877665544332211", 
                    100, 
                    "0x00",
                    1,
                    10,
                    20,
                    "0x1122334455112233445511223344551122334455",
                    "0x1122334455"
                ]);

const msgObj = { 
    pubKey: relayerPubKey, 
    sig: whisperKeyPairID,
    ttl: 1000, 
    powTarget: 1, 
    powTime: 20, 
    topic: "0x4964656e", 
    payload: web3.utils.toHex({
        'contract': "0x692a70d2e424a56d2c6c27aa97d1a86395877b3a",
        'address': web3.eth.defaultAccount
        'action': 'transaction',
        'encodedFunctionCall': funCall,
    })
};

        
web3.shh.post(msgObj)
.then((err, result) => {
    console.log(result);
    console.log(err);
});

Javascript Library

Using the file status-gas-relayer.js, setup connection to geth, and required keypairs and symkeys

import StatusGasRelayer, {Contracts, Functions, Messages} from 'status-gas-relayer';

// Connecting to web3
const web3 = new Web3('ws://localhost:8546');
const kid = await web3js.shh.newKeyPair()
const skid = await web3.shh.addSymKey("0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b");

Subscribing to messages

General message subscription. Special handling is needed for relayer availability. The sig property is the relayer's public key that needs to be sent when sending a transaction message. More than 1 relayer can reply, so it's recommended to save these keys in a list/array.

StatusGasRelayer.subscribe(web3js, (error, msgObj) => {
    if(error) {
        console.error(error);
        return;
    }

    if(msgObj.message == Messages.available){
        // Relayer availability message
        console.log("Relayer available: " + msgObj.sig);
    } else {
        // Normal message
        console.log(msgObj);
    }
}, {
    privateKeyID: kid
});

Polling for relayers

const identityAddress = this.props.identityAddress; // Identity contract
const accountAddress = web3.eth.defaultAccount;
const gasToken = SNT.options.address;
const gasPrice = 1000000000000;  // In wei equivalent to the token

const s = new StatusGasRelayer.AvailableRelayers(Contracts.Identity, identityAddress, accountAddress)
                              .setRelayersSymKeyID(skid)
                              .setAsymmetricKeyID(kid)
                              .setGas(gasToken, gasPrice);
await s.post(web3);

Signing a message

Signing a message is similar to invoking a function. Both use mostly the same functions. The difference is that when you invoke a function, you need to specify the relayer and asymmetric key Id.

try {
    const s = new StatusGasRelayer.Identity(identityAddress, accountAddress)
                                  .setContractFunction(Functions.Identity.call)
                                  .setTransaction(to, value, data)
                                  .setGas(gasToken, gasPrice, gasLimit);
                                          
    const signature = await s.sign(web3);
} catch(error){
    console.log(error);
}

Using Identity contract call function

This functionality is used when a Identity will invoke a contract function or transfer ether without paying fees

try {
    const s = new StatusGasRelayer.Identity(identityAddress, accountAddress)
                                  .setContractFunction(Functions.Identity.call)
                                  .setTransaction(to, value, data)  // 'value' is in wei, and 'data' must be a hex string
                                  .setGas(gasToken, gasPrice, gasLimit)
                                  .setRelayer(relayer)
                                  .setAsymmetricKeyID(kid);

    await s.post(signature, web3);
} catch(error){
    console.log(error);
}

Using Identity contract approveAndCall function

This functionality is used when a Identity will invoke a contract function that requires a transfer of Tokens

try {
    const s = new StatusGasRelayer.Identity(identityAddress, accountAddress)
                                  .setContractFunction(Functions.Identity.approveAndCall)
                                  .setTransaction(to, value, data)
                                  .setBaseToken(baseToken)
                                  .setGas(gasToken, gasPrice, gasLimit)
                                  .setRelayer(relayer)
                                  .setAsymmetricKeyID(kid);

    await s.post(signature, web3);
} catch(error){
    console.log(error);
}

Using SNTController transferSNT function

This functionality is used for simple wallets to perform SNT transfers without paying ETH fees

try {
    const accounts = await web3.eth.getAccounts();

    const s = new StatusGasRelayer.SNTController(SNTController.options.address, accounts[2])
                                    .transferSNT(to, amount)
                                    .setGas(gasPrice)
                                    .setRelayer(relayer)
                                    .setAsymmetricKeyID(kid);

    await s.post(signature, web3);
} catch(error){
    console.log(error);
}

Using SNTController execute function

try {
    const accounts = await web3.eth.getAccounts();

    const s = new StatusGasRelayer.SNTController(SNTController.options.address, accounts[2])
                                  .execute(allowedContract, data)
                                  .setGas(gasPrice, gasMinimal)
                                  .setRelayer(relayer)
                                  .setAsymmetricKeyID(kid);

    await s.post(signature, web3);
} catch(error){
    console.log(error);
}

Status Extensions

  • TODO