Skip to content
This repository has been archived by the owner on Jul 4, 2022. It is now read-only.

Doughnut

HaoZhaiX edited this page May 3, 2020 · 20 revisions

Doughnuts are Proofs of Delegation between two or more cryptographic keypairs. Doughnuts let us prove that one address delegates something to another address.

For example, if Alice wants to let Bob use her account, but only to send tokens to Charlie. This could be achieved using a doughnut by doing the following:

  • Alice creates a doughnut which sets:
    • Alice as the issuer
    • Bob as the holder
    • Sending funds to Charlie as a rule in the permission domain
  • Alice signs the doughnut and gives it to Bob
  • When Bob wants to send funds to Charlie with Alice's account, Bob should attach the doughnut when signing and sending an extrinsic.

This guide covers:

  1. How to create a doughnut in your D'App using the Javascript SDK
  2. How to send an encoded doughnut from your D'App to a PL^G blockchain
  3. How to write a DispatchVerifier hook on your blockchain to interpret a doughnut

Using Javascript SDK to create a doughnut

The following instructions will help you use the Javascript SDK to play with doughnut.

1. Install plug-doughnut

Run npm or yarn command to install the plug-doughnut package.

npm install plug-doughnut or yarn add plug-doughunut

2. Create doughnut

Use the Doughnut builder pattern to create a doughnut with necessary fields. (Note: the issuer and holder should be set to the account public keys)

  // Doughnut THINGs
  const Doughnut = require('plug-doughnut').Doughnut;
  const testingPairs = require('@polkadot/keyring/testingPairs');

  const keyring = testingPairs.default({ type: 'ed25519'});
	 
  const issuer = keyring.alice.publicKey;
  const issuer_private_key = keyring.alice.sign();
  const holder = keyring.bob.publicKey;
  const expiry = 100;
  const not_before = 1;

  const doughnut = Doughnut
    .new(issuer, holder, expiry, not_before)
	.add_payload_version(1)
	.add_domain('awesome', [1, 2, 3]);
  const signature = keyring.alice.sign(doughnut.payload());
  doughnut.add_signature(signature);

For doughnuts to be valid, they must be signed by the issuer.

You can access the fields in the doughnut with getter functions:

const issuer = doughnut.issuer;
const holder = doughnut.holder;
const expiry = doughnut.expiry;

3. Encode the doughnut

To send a doughnut, it must be encoded as a binary object. Doughnuts are encoded as u8 arrays by using doughnut.encode().

Encoded doughnut binaries can be decoded into Doughnut objects using Doughnut.decode([u8]).

const encoded_doughnut = doughnut.encode();
const doughnut = Doughnut.decode(encoded_doughnut);

An encoded doughnut is a Uint8Array, for example:

[
 64, 24, 64, 22, 126, 150, 15, 176, 190, 210, 156, 179, 149, 142, 84, 153, 4, 203, 61, 62,
  185, 76, 45, 162, 220, 254, 188, 163, 187, 63, 39, 186, 113, 126, 12, 60, 121, 179, 67,
  105, 121, 244, 39, 137, 174, 55, 85, 167, 73, 111, 50, 249, 10, 145, 141, 125, 105, 138,
  38, 93, 144, 45, 224, 70, 206, 246, 116, 196, 94, 16, 0, 115, 111, 109, 101, 116, 104, 105,
  110, 103, 0, 0, 0, 0, 0, 0, 0, 128, 0, 115, 111, 109, 101, 116, 104, 105, 110, 103, 69,
  108, 115, 101, 0, 0, 0, 128, 0, 0, 0, 8, 185, 184, 138, 72, 86, 187, 125, 166, 109, 176,
  31, 104, 162, 235, 78, 157, 166, 8, 137, 191, 33, 202, 128, 138, 165, 73, 244, 67, 247, 37,
  13, 218, 44, 244, 54, 137, 179, 56, 110, 152, 170, 180, 218, 107, 177, 170, 58, 91, 62, 24,
  240, 248, 244, 13, 51, 235, 3, 21, 63, 79, 192, 137, 6
]

Send doughnut to blockchain

1. Create a signed and encoded doughnut

const Doughnut = require('plug-doughnut').Doughnut;
const domain = 'awesome';

const doughnut = Doughnut
  .new(issuer, holder, expiry, not_before)
  .add_payload_version(1)
  .add_domain('awesome', [1, 2, 3]);
const signature = keyring.alice.sign(doughnut.payload());
doughnut.add_signature(signature);

const encoded_doughnut = doughnut.encode();

2. Add the encoded doughnut to an extrinsic

We will do a balances.transfer and add the encoded doughnut in the option parameter.

// Create the API and wait until ready
const provider = new WsProvider("ws://localhost:9944");
const types = PlugRuntimeTypes.default;
const api = await ApiPromise.create({ provider, types });

// Send transfer extrinsic with doughnut
const options = { doughnut: doughnut.encode() };
const txHash = await api.tx.balances
  .transfer(keyring.charlie.address, 1500000000)
  .signAndSend(keyring.bob, options);

Interpreting Doughnuts on the PL^G Blockchain

The following sections assume we have completed the Build a New Project with PL^G Getting Started Guide.

1. Get domain through dispatch verifier

Doughnuts contain permission domains so that they can be used across many applications. For example, if Alice wants to enable permission for Bob to use her account in both "cennznet" and "awesome" blockchains, then Alice will need to add both permission domains to the doughnut. In our example, we will only add the permission domain for the "awesome" blockchain.

When a doughnut is sent with an extrinsic to the blockchain, it is passed to a verify_dispatch function along with:

  • The name of the runtime module called (eg/ "balances")
  • The name of the method called (eg/ "transfer")
  • A list of arguments passed (eg/ [ charlie_pub_key, 1_500_000_000 ])

First, we will look at how to check the doughnut domains in the verify_dispatch function which is implemented in the plug-blockchain.

In node-template/runtime/src/lib.rs we define the DelegatedDispatchVerifier as DummyDispatchVerifier.

type DelegatedDispatchVerifier = DummyDispatchVerifier<Self::Doughnut, Self::AccountId>;

For a production application, we would define our own DispatchVerifier (maybe AwesomeDispatchVerifier); but for this example, we will modify the verify_dispatch function for the DummyDispatchVerifier in frame/support/src/additional_traits.rs to print out the domain data:

use sp_runtime::{Doughnut, DoughnutV0};
use log::{trace};

fn verify_dispatch(doughnut: &Self::Doughnut, module: &str, method: &str, args: Vec::<(&str, &dyn Any)>) -> Result<(), &'static str> {
    let doughnut = DoughnutV0::try_from(doughnut.clone()).unwrap();
    let domain = doughnut.get_domain("awesome").unwrap()[0];
    trace!("awesome domain's value: {}", domain);

    Ok(());
}

After sending the transfer extrinsic to the local node. We should see the following output in the console log:

awesome domain's value: 0

2. Configuring the permission domain to block/allow transactions

We can set a permission domain specific for our awesome_node in the DelegatedDispatchVerifier implementation for DummyDispatchVerifier. Only doughnuts containing the awesome domain will be verified.

impl<D: PlugDoughnutApi, A: Parameter> DelegatedDispatchVerifier for DummyDispatchVerifier<D, A> {
	type Doughnut = D;
	type AccountId = A;

        // The doughnut permission domain it verifies
	const DOMAIN: &'static str = "awesome";

	fn verify_dispatch(doughnut: &Self::Doughnut, module: &str, method: &str, args: Vec::<(&str, &dyn Any)>) -> Result<(), &'static str> {
            let doughnut = DoughnutV0::try_from(doughnut.clone()).unwrap();
            let domain = doughnut.get_domain(Self::DOMAIN).ok_or("Doughnut does not grant permission for awesome_node domain")?;
            trace!("Permission domain for awesome_node is verified, domain value: {}", domain);

            Ok(());
        }
}

Try sending transactions with and without the awesome domain included. Only doughnuts which contain domain: "awesome" will be processed successfully.