Working with Lit Actions

An introduction to building Lit-enabled infrastructure with PKPs and Lit Actions.

Working with Lit Actions

Introduction: What is Lit Protocol?

Lit Protocol is a decentralized key management network that can be used to power encryption, access control, and computation on the open web. Lit is not a blockchain, but a blockchain-agnostic middleware layer that can be used to read and write data between distributed systems and off-chain platforms.

Developers can use Lit Protocol in their application backend — whether it be introducing privacy to previously public-by-default systems, or automating user interactions with methods of wallet abstraction and seamless auth flows.

In this article, we will be exploring how you can implement Lit infrastructure on a technical level via Lit Actions.

Let’s dive in!

What are Lit Actions?

Lit Actions are immutable JavaScript programs stored on IPFS. They can be thought of as our native implementation of smart contracts, only much more powerful.

Lit Actions are blockchain agnostic, giving them the inherent capacity to communicate data across blockchains. This enables interoperability between previously disconnected ecosystems.

Their second most powerful characteristic is their ability to use off-chain data sources in their computation. Lit Actions can make arbitrary HTTP requests, for example fetching off-chain data or calling an API. Functionally, this gives developers the ability to instill “oracle” or “bridging” capabilities into their blockchain applications with ease from a single development environment.

How do they work?

Lit Actions work directly with Programmable Key Pairs (PKPs), which you can read more about in our docs. A PKP is an ECDSA key pair generated collectively by the Lit nodes in a process called distributed key generation. This key pair is minted in the form of an ERC-721 NFT, meaning the owner of the NFT becomes the controller of the key pair*. This controller can then grant Lit Actions the permission to sign using their PKP, creating a distributed, programmatic cloud wallet.

This also has interesting implications in the context of generating proofs. We created a process called “Mint-Grant-Burn” where one:

  1. Mints a PKP.
  2. Grants a Lit Action permission to sign using that PKP.
  3. Burns the PKP.

This ensures that you can trust data that is signed by the Lit network. For example:

Say you have a Lit Action and corresponding PKP that checks if a number is prime, and only signs it if it is prime. Think of it as a prime number certification service. The Lit Action is immutable, and since that PKP is permanently assigned to that Lit Action (via Mint-Grant-Burn), there is a provable chain of trust. This means you could present the signature and a number to someone, and they could simply check the signature against the public key of the PKP to see if the number is actually prime, instead of having to do all the math to ensure that the number is actually prime. The signature acts as a proof that the number is prime.

Working with Lit Actions

Below, we will walk through examples of the functionality that Lit Actions provide. You can view the new Lit Actions SDK docs here.

To begin, start by installing the Lit JS SDK serrano tag:

yarn add lit-js-sdk@serrano

This is our PKPs and Lit Actions Testnet. The data on Serrano is not persistent and may be erased at any time. Therefore, we do not recommend storing anything of value on this network.


Hello World

This example uses the function signECDSA . It allows you to query the Lit nodes for an ECDSA signature share. These shares can be collected and combined by an authorized client (via auth sig) to form the complete signature.

const go = async () => {
  // this is the string "Hello World" for testing
  const toSign = [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100];
  // this requests a signature share from the Lit Node
  // the signature share will be automatically returned in the HTTP response from the node
  const sigShare = await Lit.Actions.signEcdsa({
    toSign,
    publicKey:
      '0x02e5896d70c1bc4b4844458748fe0f936c7919d7968341e391fb6d82c258192e64',
    sigName: 'sig1',
  });
};

go();

This code is executed by all of the Lit nodes in parallel. You’ll need additional code to send the above JS to the nodes, collect the signature shares, combine them, and print the signature. In the following, we store the above code into a variable called litActionCode. We then execute it, obtain the signature, and print it:

import LitJsSdk from "lit-js-sdk";

// this code will be run on the node
const litActionCode = `
const go = async () => {  
  // this requests a signature share from the Lit Node
  // the signature share will be automatically returned in the HTTP response from the node
  // all the params (toSign, publicKey, sigName) are passed in from the LitJsSdk.executeJs() function
  const sigShare = await Lit.Actions.signEcdsa({ toSign, publicKey , sigName });
};

go();
`;

const runLitAction = async () => {
  // you need an AuthSig to auth with the nodes
  // this will get it from metamask or any browser wallet
  const authSig = await LitJsSdk.checkAndSignAuthMessage({ chain: "ethereum" });

  const litNodeClient = new LitJsSdk.LitNodeClient({ litNetwork: "serrano" });
  await litNodeClient.connect();
  const signatures = await litNodeClient.executeJs({
    code: litActionCode,
    authSig,
    // all jsParams can be used anywhere in your litActionCode
    jsParams: {
      // this is the string "Hello World" for testing
      toSign: [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100],
      publicKey:
        "0x02e5896d70c1bc4b4844458748fe0f936c7919d7968341e391fb6d82c258192e64",
      sigName: "sig1",
    },
  });
  console.log("signatures: ", signatures);
};

runLitAction();

Conditional Signing

You can use the Lit network as a sort of “proof generation” system, obtaining a signature from the Lit nodes upon their observation of a specific condition. This powerful condition-checking property was inherited from our access control product.

Below, we will write a Lit Action that checks if a user has at least 1 wei on Ethereum, and only return a signature if they do:

import LitJsSdk from "lit-js-sdk/build/index.node.js";

// this code will be run on the node
const litActionCode = `
const go = async () => {
  // test an access control condition
  const testResult = await Lit.Actions.checkConditions({conditions, authSig, chain})

  console.log('testResult', testResult)

  // only sign if the access condition is true
  if (!testResult){
    return;
  }

  // this is the string "Hello World" for testing
  const toSign = [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100];
  // this requests a signature share from the Lit Node
  // the signature share will be automatically returned in the HTTP response from the node
  const sigShare = await LitActions.signEcdsa({ toSign, publicKey: "0x02e5896d70c1bc4b4844458748fe0f936c7919d7968341e391fb6d82c258192e64", sigName: "sig1" });
};



go();
`;

// you need an AuthSig to auth with the nodes
// normally you would obtain an AuthSig by calling LitJsSdk.checkAndSignAuthMessage({chain})
const authSig = {
  sig: "0x2bdede6164f56a601fc17a8a78327d28b54e87cf3fa20373fca1d73b804566736d76efe2dd79a4627870a50e66e1a9050ca333b6f98d9415d8bca424980611ca1c",
  derivedVia: "web3.eth.personal.sign",
  signedMessage:
    "localhost wants you to sign in with your Ethereum account:\n0x9D1a5EC58232A894eBFcB5e466E3075b23101B89\n\nThis is a key for Partiful\n\nURI: https://localhost/login\nVersion: 1\nChain ID: 1\nNonce: 1LF00rraLO4f7ZSIt\nIssued At: 2022-06-03T05:59:09.959Z",
  address: "0x9D1a5EC58232A894eBFcB5e466E3075b23101B89",
};

const runLitAction = async () => {
  const litNodeClient = new LitJsSdk.LitNodeClient({
    litNetwork: "serrano",
  });
  await litNodeClient.connect();
  const signatures = await litNodeClient.executeJs({
    code: litActionCode,
    authSig,
    jsParams: {
      conditions: [
        {
          conditionType: "evmBasic",
          contractAddress: "",
          standardContractType: "",
          chain: "ethereum",
          method: "eth_getBalance",
          parameters: [":userAddress", "latest"],
          returnValueTest: {
            comparator: ">=",
            value: "1",
          },
        },
      ],
      authSig: {
        sig: "0x2bdede6164f56a601fc17a8a78327d28b54e87cf3fa20373fca1d73b804566736d76efe2dd79a4627870a50e66e1a9050ca333b6f98d9415d8bca424980611ca1c",
        derivedVia: "web3.eth.personal.sign",
        signedMessage:
          "localhost wants you to sign in with your Ethereum account:\n0x9D1a5EC58232A894eBFcB5e466E3075b23101B89\n\nThis is a key for Partiful\n\nURI: https://localhost/login\nVersion: 1\nChain ID: 1\nNonce: 1LF00rraLO4f7ZSIt\nIssued At: 2022-06-03T05:59:09.959Z",
        address: "0x9D1a5EC58232A894eBFcB5e466E3075b23101B89",
      },
      chain: "ethereum",
    },
  });
  console.log("signatures: ", signatures);
};

runLitAction();

Fetching Off-chain Data

Lit Actions can natively talk to the external world, giving them the inherent capabilities of an oracle system. You can pull data off of the web, or push requests to external APIs.

The example below will get the current temperature from a weather API, and only sign a txn if the temperature is forecast to be above 60Fº. The request and response logic are baked into the Lit Action itself, meaning you don’t have to rely on a third-party service. The HTTP request will be sent out by all the Lit Nodes, and consensus is based on at least 2/3 of the nodes getting the same response. If less than 2/3 nodes get the same response, then the user can not collect the signature shares above the threshold and therefore cannot produce the final signature.

import LitJsSdk from "lit-js-sdk/build/index.node.js";

// this code will be run on the node
const litActionCode = `
const go = async () => {  
  const url = "<https://api.weather.gov/gridpoints/TOP/31,80/forecast>";
  const resp = await fetch(url).then((response) => response.json());
  const temp = resp.properties.periods[0].temperature;

  // only sign if the temperature is above 60.  if it's below 60, exit.
  if (temp < 60) {
    return;
  }
  
  // this requests a signature share from the Lit Node
  // the signature share will be automatically returned in the HTTP response from the node
  // all the params (toSign, publicKey, sigName) are passed in from the LitJsSdk.executeJs() function
  const sigShare = await LitActions.signEcdsa({ toSign, publicKey , sigName });
};

go();
`;

// you need an AuthSig to auth with the nodes
// normally you would obtain an AuthSig by calling LitJsSdk.checkAndSignAuthMessage({chain})
const authSig = {
  sig: "0x2bdede6164f56a601fc17a8a78327d28b54e87cf3fa20373fca1d73b804566736d76efe2dd79a4627870a50e66e1a9050ca333b6f98d9415d8bca424980611ca1c",
  derivedVia: "web3.eth.personal.sign",
  signedMessage:
    "localhost wants you to sign in with your Ethereum account:\\n0x9D1a5EC58232A894eBFcB5e466E3075b23101B89\\n\\nThis is a key for Partiful\\n\\nURI: <https://localhost/login\\nVersion:> 1\\nChain ID: 1\\nNonce: 1LF00rraLO4f7ZSIt\\nIssued At: 2022-06-03T05:59:09.959Z",
  address: "0x9D1a5EC58232A894eBFcB5e466E3075b23101B89",
};

const runLitAction = async () => {
  const litNodeClient = new LitJsSdk.LitNodeClient({
    alertWhenUnauthorized: false,
    litNetwork: "serrano",
    debug: true,
  });
  await litNodeClient.connect();
  const signatures = await litNodeClient.executeJs({
    code: litActionCode,
    authSig,
    // all jsParams can be used anywhere in your litActionCode
    jsParams: {
      // this is the string "Hello World" for testing
      toSign: [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100],
      publicKey:
        "0x02e5896d70c1bc4b4844458748fe0f936c7919d7968341e391fb6d82c258192e64",
      sigName: "sig1",
    },
  });
  console.log("signatures: ", signatures);
};

runLitAction();

Multiple Parameters

This example Lit Action will ask the nodes to independently sign a series of pre-specified conditions. A signature will only be returned if the inputs match all of the specified parameters.

import LitJsSdk from "lit-js-sdk/build/index.node.js";

// this code will be run on the node
const litActionCode = `
const go = async () => {
  // this is the string "Hello World" for testing
  const toSign = [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100];
  // this requests a signature share from the Lit Node
  // the signature share will be automatically returned in the HTTP response from the node
  const sigShare = await LitActions.signEcdsa({ toSign, publicKey: "02c55050b2f1a2f3207452f7a662728ed68f6dd17b8060b07d3af09a801b02f3c0", sigName: "sig1" });
};

// check the params we passed in:
const correctParams = {
  aString: "meow",
  anInt: 42,
  aFloat: 123.456,
  anArray: [1, 2, 3, 4],
  anArrayOfStrings: ["a", "b", "c", "d"],
  anObject: { x: 1, y: 2 },
  anObjectOfStrings: { x: "a", y: "b" },
};

// abort if any of these mismatch.  the signing won't happen.
if (
  aString !== correctParams.aString ||
  anInt !== correctParams.anInt ||
  aFloat !== correctParams.aFloat ||
  JSON.stringify(anArray) !== JSON.stringify(correctParams.anArray) ||
  JSON.stringify(anArrayOfStrings) !==
    JSON.stringify(correctParams.anArrayOfStrings) ||
  JSON.stringify(anObject) !== JSON.stringify(correctParams.anObject) ||
  JSON.stringify(anObjectOfStrings) !==
    JSON.stringify(correctParams.anObjectOfStrings)
) {
  // noooo
  console.log("------------- HEY!  Notice this! -------------");
  console.log("One of the params passed in is not matching");
  console.log("correctParams", JSON.stringify(correctParams));
  console.log(
    "Go and figure out which one below isn't matching correctParams above to debug this"
  );
  console.log("aString: ", aString);
  console.log("anInt: ", anInt);
  console.log("aFloat: ", aFloat);
  console.log("anArray: ", anArray);
  console.log("anArrayOfStrings: ", anArrayOfStrings);
  console.log("anObject: ", anObject),
    console.log("anObjectOfStrings: ", anObjectOfStrings);
  console.log(
    "------------- EXITING LIT ACTION due to above error -------------"
  );
  process.exit(0);
} else {
  console.log('all the params match!')
}

go();
`;

// you need an AuthSig to auth with the nodes
// normally you would obtain an AuthSig by calling LitJsSdk.checkAndSignAuthMessage({chain})
const authSig = {
  sig: "0x2bdede6164f56a601fc17a8a78327d28b54e87cf3fa20373fca1d73b804566736d76efe2dd79a4627870a50e66e1a9050ca333b6f98d9415d8bca424980611ca1c",
  derivedVia: "web3.eth.personal.sign",
  signedMessage:
    "localhost wants you to sign in with your Ethereum account:\\n0x9D1a5EC58232A894eBFcB5e466E3075b23101B89\\n\\nThis is a key for Partiful\\n\\nURI: <https://localhost/login\\nVersion:> 1\\nChain ID: 1\\nNonce: 1LF00rraLO4f7ZSIt\\nIssued At: 2022-06-03T05:59:09.959Z",
  address: "0x9D1a5EC58232A894eBFcB5e466E3075b23101B89",
};

const go = async () => {
  const litNodeClient = new LitJsSdk.LitNodeClient({
    alertWhenUnauthorized: false,
    minNodeCount: 6,
    bootstrapUrls: [
      "<http://localhost:7470>",
      "<http://localhost:7471>",
      "<http://localhost:7472>",
      "<http://localhost:7473>",
      "<http://localhost:7474>",
      "<http://localhost:7475>",
      "<http://localhost:7476>",
      "<http://localhost:7477>",
      "<http://localhost:7478>",
      "<http://localhost:7479>",
    ],
    litNetwork: "custom",
    debug: true,
  });
  await litNodeClient.connect();
  const signatures = await litNodeClient.executeJs({
    code: litActionCode,
    authSig,
    jsParams: {
      aString: "meow",
      anInt: 42,
      aFloat: 123.456,
      anArray: [1, 2, 3, 4],
      anArrayOfStrings: ["a", "b", "c", "d"],
      anObject: { x: 1, y: 2 },
      anObjectOfStrings: { x: "a", y: "b" },
    },
  });
  console.log("signatures: ", signatures);
};

go();

Initial Implementations

Here are some example applications using Lit Actions and PKPs.

Sling Protocol: An SDK for automating interactions with decentralized exchanges, enabling things like on-chain limit orders.

React App: A basic app that displays a signed response if certain conditions are met.

More implementations here.

Additional Resources

You can find even more examples within our docs and on GitHub.

If you’re interested in contributing within the Lit community, check out our Ecosystem Proposals and Grants program.

For developer support or questions, join our Discord.

*Each node in the Lit network holds key “shares”. These shares must be aggregated by an authorized client (the key controller) in order to be used for things like signing and decryption. This means that on their own, these shares are worthless. You can read more about decentralized custody and authorization in our docs.