CashRPC - Protocol to attempt to unify Wallet/App comms

Hi all,

This is a follow-up from the WC2 thread here:

https://bitcoincashresearch.org/t/wallet-connect-v2-support-for-bitcoincash/1100/20

There were some use-cases I couldn’t cover with the existing WC2 implementation so I’ve attempted to start on an underlying RPC protocol that (hopefully) can be used across different transports (currently draft implementation for WC2 - and, in future, will do HTTP and LibP2P).

The intent here is to try to standardize a versatile (and, to some degree, secure) protocol that will cover MOST current use-cases (although, not all. Feedback on how we might be able to do this better is most welcome).

This is heavy WIP, but I’ve created a tool to demonstrate the rough concept. This uses BCHouse’s Flipstarter as a demo. I’ve been in contact with BCH Guru Dev and have permission to demonstrate their contract with this tool too - I’ll post a project file here in a day or two.

The tool itself is available here:

9 Likes

Some scattered notes:

Usage Notes

  1. Set Cashonize to use CHIPNET (if you need some Chipnet coinage, message me on telegram: Telegram: Contact @jimtendo (there is a link to the Cashonize Fork that works with this near the top-right).
  2. There’s a help button near the top right on most pages of the tool that provides additional info for each component.
  3. The Project format is not final, but I will make old versions of this tool available so that you can migrate to new formats.
    1. If possible, I’ll try to do this automatically upon import (but please do not depend upon me doing this).
  4. This is what I consider a pre-alpha release. There will probably be many bugs still (and breaking changes incoming).
  5. I’ve included two examples:
    1. Sahid’s Flipstarter (this contract is really nice and simple to follow and demonstrates how Token Genesis can be done)
    2. Early BCH Guru Contracts (these are earlier versions I was sent - but they demonstrate some advanced capabilities that LibAuth Templates give us)
  6. Transaction Validation is built into CashRPC.
    1. If a Transaction Fails to build, you will get an error message and you can view the full stack trace for each input in your Cashonize Wallet’s Browser Console.
  7. This tool is currently restricted Chipnet.

Brief Overview

  1. To help preserve privacy (inc. with HD Wallets) and improve security, the Session established between DAPP (Service) and WALLET uses a Sandboxed Keypair generated using sandboxedPrivateKey = sha256(${masterPrivateKey}cashrpc-over-wc:${domainName}), where:
    1. For non-HD Wallets, the masterPrivateKey is just the key of the Wallet.
    2. For HD Wallets, the masterPrivateKey can be a derivation path of the Wallet’s seed.
  2. This uses LibAuth templates. In future, these template can be hashed and whitelisted (trusted) by a wallet.
  3. For security, the service can only specify inputs and outputs by their corresponding scripts in the provided template (no raw locking/unlocking bytecode allowed). In this sense, there are currently four different kinds of Inputs and Outputs:
    1. Template-scoped Inputs (uses a script from template)
    2. Template-scoped Outputs (uses a script from template)
    3. Wallet-scoped Inputs (must contain a Whitelisted Token Category - see below)
    4. Wallet-scoped Outputs (A Change Ooutput - Lockscript will be provided by Wallet)
  4. CashRPC will provide additional UTXOs to automatically meet the Satoshi and Fungible Token amounts that are summed in the outputs. In this respect, UTXO selection is done by the Wallet, but the DAPP can also provide its own inputs (and whitelisted NFTs from the wallet: see next point).
  5. The DAPP CAN request a WALLET’s UTXOs provided they a) contain tokens and b) access to those token category ID’s have been granted upon Session Negotiation (allowedTokens as an array of category ids or * for ALL tokens). This allows us to support token-specific use-cases (e.g. a particular NFT as input or more vague Crypto-Exchange use-cases).
  6. To prevent standards fracturing, the underlying RPC’s are intended to be transport agnostic: They should be applicable to HTTP, LibP2P any other transports (not just WalletConnect). Thus, we should be able to use them for a future HTTP-based Payment Protocols, etc.
  7. Hopefully, in future when we have stateful wallets, we will be to store the AddressData of the transactions and converge with what BitJSON is building. The idea is that a wallet CAN (if the template is natively supported) save the data payloads and use the built-in Wallet UX to execute contract actions. But, for WC, the idea is that the DAPP (Service) stores the data associated with a given WC account.
  8. Type-hints (for Wallet Display of Data Variables) are currently stored as plaintext in the “description” field of a variable in the template. This is a temporary workaround.
    1. We want to think a little about how to handle this as they are not always static (e.g. using oracles.cash, we may set a price - but we want that to show depending upon which Oracle was chosen in another variable according to the scaling factor). Therefore, we ideally want some “intelligence” to these typehints.
  9. The Sandboxed Accounts are currently identified by Public Key (not address) in the form bch:${chain}:${publicKey}.
    1. This does break from WC convention but simplifies implementation.
    2. It also discourages DAPPs from sending to funds to these SANDBOXED accounts.
    3. It might give us interesting capabilities wrt signing/encryption (ECIES) within a service.
  10. There are a few limitations currently with this approach:
    1. Multi-party contracts are either not possible or very clunky to implement. There’s a constraint currently that the Wallet MUST be able to sign a transaction successfully. I haven’t worked out a better approach for this yet, but maybe someone might have suggestions for a more versatile approach.

Implementation

  1. All code is available under the top-right Git Icon.
  2. There are currently three different repositories (but there will be four in future).
    1. bch-wc2-experimental (The CashRPC over WC Library)
    2. cashonize (The fork of Cashonize that implements CashRPC over WC)
    3. CashRPC IDE (the tool built using Quasar)
    4. (TODO) CashRPC (In future, this will be split out from the Wallet Connect implementation for use in Payment Protocols and even internal Wallet Use)
  3. Please do not submit MRs quite yet. There is a lot of refactoring I still need to do and a lot of mess to cleanup.
  4. I’m putting a lot of effort into trying to make CashRPC over WC very easy for Web Wallets to implement via the library.
    1. Excluding UI, it only takes around 150 LoC - see wallet-connect-2.js in Cashonize repository and Ctrl+F WalletConnectService.
    2. DO NOT try to implement in your Wallet yet. There’s a number of changes I need to make still (see TODOs below).
  5. For Dapp Developers, there will be a WalletConnectDapp class that tries to handle some of the quirkier aspects of WalletConnect.
    1. You are NOT bound to this - but it should ease implementation and (hopefully) handle some of the difficult edge-cases.

TODOs (Big Breaking Changes)

  1. Tests will eventually live in the “template” under “actions”. We want to do this for security:
    1. Some contract flows expect a Payout Address. For security, we do not want the script for this to be callable directly, but only with a corresponding Unlock (for example).
  2. I would like to find a way to stash metadata about which Inputs belong to the Wallet in the response payloads (to ease Wallet UI Implementations).
  3. Separate CashRPC into its own Library (and add validations).
  4. Many refactors/cleanups in the code (it is a mess currently).
  5. Casting of values in the tool is very clunky. I’ll probably rewrite to use JS (as opposed to JSON) and make casting explicit.
  6. Not a breaking change, but the Cashonize UI for this needs A LOT of work.

“Actions” (WRT TODOs Point 1)

The format for these will be similar to Tests. Format will probably look something like:

"actions: {
  "create_campaign": {
    "name": "Some Action",
    "description": "...",
    "signerKey": "user",
    "transactions": [
      // ...
    ]
  }
}
6 Likes

I took a first look at the CashRPC IDE, and it looks great!

So if I get this right we can fully test a P2SH version of a Flipstarter campaign, including the campaign creation, 2 pledges and the final Campaign payout.

I used the CashRPC WalletConnect to connect the CashRPC fork of Cashonize to the IDE.
It’s a great showcase of what actually changed compared to the current WalletConnect used.

It’s awesome that it can show the Template, the permissions of the app (Methods) and then which tokens it can touch on the session approval!

Compared to the current:

When creating a test campaign the sign transaction screen looks like this:
It’s great UX to see the 2 separate transactions actually being labeled so I a user can know this is

  1. Creating a Category Genesis Outpoint
  2. Creating a Flipstarter Campaign

So this is a big improvement over the current standard! Still need to look closer into the Libauth Authentication Template generation and how the actual tests work, but very positive so far!

3 Likes

Just a small update:

  1. I realized I’d screwed up the default project by including an unsupported RPC method. If you tried to connect and it failed, do a hard reload and then in the top left menu icon, click “New Project” (this should then load the latest “fixed” default project).
  2. I’ve fixed a few bugs on the wallet-side to do with UTXO selection (but haven’t pushed these up to the repo yet). There’s still some changes incoming to the Library.
  3. We want to preserve compatibility with existing WC implementations. So, I’ve decided to brand this “CashConnect” connect for now (and will try to PR into Cashonize late this month to support both this and the original WC implementation).

WRT the branding, the idea is that the underlying RPC’s will be transport agnostic. So, different combinations of transport + protocol will probably have different names. E.g.

CashRPC + WalletConnect = CashConnect.
CashRPC + HTTP = CashPaymentProto.
CashRPC + LibP2P = ???
CashRPC + Intra-Wallet-Use = Probably just “CashRPC”

This WILL change (so don’t attempt implementing this yet), but an example of how the Wallet Integration code will look when using the library is here:

It’s pretty simple - there are only a few callbacks required (getUnspents, getSourceOutput and getChangeTemplate). The library will take care of everything else.

I’ve gotten permission from BCH Guru Dev to share a project file containing their contracts, but I haven’t had time to put this together yet. Will likely come next week.

3 Likes

you realize that “RPC” is short for “Remote Procedure Call”, right? In other words, it is about exposing a private API to be used over process boundaries.

I"m sorry for being difficult, my question stems from the lack of high level documentation. The basic idea we started with was a normal wallet interoperating with a foreign service run by some 3rd party. Where said 3rd party was able to command the wallet to do things.
The later information gives conflicting info, like a mention of a payment protocol. I was baffled by the mention of payment protocol usage, which is definitely an entirely different beast from RPC.

It might not be the most suitable name (I’m open to suggestions).

To elaborate a little bit on why I went with it though, CashRPC is like a sandbox. It gives apps (intra-wallet or external) an API with which it can communicate with the main wallet. It also allows for creating a Sandboxed Keypair (this will be used, primarily, by the external apps - but there might be intra-wallet use-cases too).

While this isn’t technically “inter-process communication” (in the typical sense of the term anyway), conceptually I thought it was somewhat similar. And there may be future use-cases (or particular Wallet architectures) where it is used in such a manner. E.g. imagine a stand-alone authentication app that leverages a Wallet to sign into an online service - or a Wallet architecture where the UI is distinct from the underlying daemon (WebWallets will likely want to transition to this type of architecture eventually where the daemon becomes the ServiceWorker).

I do apologize that my high-level descriptions are not very good. There are many things I am still fleshing out and I haven’t quite worked out how to give a good, succinct, textual description of it. I’m hoping that once I make some better demos (and maybe tutorials), things’ll become a bit clearer.

2 Likes

The idea of RPC is the opposite of a sandbox. :man_shrugging:

Cool, will wait for that!

CashConnect has now been merged into the mainline Cashonize wallet!

Big thanks to @jimtendo for designing the specification, libraries, developer tooling and wallet integration to make the new protocol take off! ! :boom:

Made a social media announcement post from Cashonize detailing some of the specifics :scroll:

4 Likes

Just want to drop an update on progress:

Fundamentally, CashConnect uses LibAuth templates. There is an additional top-level actions object I plan on adding which will define the “transaction shapes” (NOTE: “transaction shapes” need only define the REQUIRED inputs/outputs to satisfy the Smart Contract constraints - CashConnect does additional UTXO selection automatically and will append appropriate change outputs).

{
  "entities" {
    // ... Entity Names/Descriptions and their variables 
  },
  "scripts": {
    // ... CashASM Scripts
  },
  "actions": {
    // These will define the transactions/transaction shapes available.
  }
}

We do want something like this for security: Many smart contracts have a payoutLockingBytecode or similar argument. If we allow arbitrary transactions to be defined on-the-fly (outside of the template), the script responsible for executing this payoutLockingBytecode argument can be invoked by a malicious service to pay money to itself (i.e. the malicious service calls the RPC and provides a transaction containing a SINGLE OUTPUT that pays to itself). As we want to be able to hash templates and have wallets mark them as trusted, this presents a security risk.

Actions will allow us to constrain calling a script like this by ensuring that scripts can only be called within the context of one of these provided actions (transaction shapes). Thus, the script executing payoutLockingBytecode could only be called in conjunction with the appropriate input - meaning, even if one of these templates was used by a malicious service, they would still be constrained to the intended flows defined in the template.

Using the BCH Guru contract (still forthcoming, sorry) as an example, the format I will likely give these will look as follows:

{
  "entities": { ... },
  "scripts": { ... },
  "actions": {
    "bch.create_offer": {
      "name": "Create Offer",
      // The key from variables that will be used to sign this transaction (this might be removed and just default to "wallet" in future. Unsure yet.
      "signer": "player_1_key",
      "transactions": [
        {
          "outputs": [
            {
              "script": "pvp_bch_offer.lock",
              // We support CashASM in transaction fields so that amounts can be deterministic in the template itself.
              "valueSatoshis": "<player_2_amount> 4000 OP_ADD",
              "resolve": {
                "player_1_pubkey_hash": "$(<player_1_key.public_key> OP_HASH160)",
                // The below embeds the locking bytecode for the game contract as a parameter to the locking script for offer.
                // There's a bit of dirtyness here that I can't see a way out of:
                // For these contracts, we need the wallet to provide the resolved values for player_1_datasig.
                // So, we have to give it a name, but it would be nicer if we could somehow "nest" it under player_1_datasig_hash.
                "game_contract_bch_locking_bytecode": {
                  "script": "pvp_bch_game.lock",
                  "resolve": {
                    "player_1_prediction_raw": "$(<player_1_prediction> <player_1_prediction_nonce> OP_CAT)",
                    "player_1_datasig": "$(<player_1_key.schnorr_data_signature.resolved.player_1_prediction_raw>)",
                    "player_1_datasig_hash": "$(<resolved.player_1_datasig> OP_SHA256)",
                    "player_1_pubkey": "$(<player_1_key.public_key>)",
                  }
                }
              }
            }
          ]
        }
      ]
    },
  }
}

The above will actually be transpiled down a little bit: A clone of script in each transaction will be added to the top-level scripts object with the variables under resolve injected. So, for example, pvp_bch_offer.lock might become pvp_bch_offer.lock.wallet and the following will be added to the template:

{
  "scripts": {
    // ...
    "pvp_bch_offer.lock.wallet": {
      "script": "<player_2_amount> <player_1_locking_bytecode> <resolved.player_1_pubkey_hash> <resolved.game_contract_bch_locking_bytecode> <expiry_timestamp> $(<pvp_bch_offer.bytecode>)",
      "lockingType": "p2sh32"
    },
    "pvp_bch_game.lock.wallet": {
      "script": "<player_1_locking_bytecode> <resolved.player_1_datasig_hash> <resolved.player_1_pubkey> <oracle_public_key> <oracle_fee_locking_bytecode> <guru_fee_locking_bytecode> <maturity_timestamp> $(<pvp_bch_game.bytecode>)",
      "lockingType": "p2sh32"
    },
  },
  "actions": {
    "bch.create_offer": {
      "name": "Create Offer",
      "signer": "player_1_key",
      "transactions": [
        {
          "outputs": [
            {
              "script": "pvp_bch_offer.lock.wallet",
              "valueSatoshis": "<player_2_amount> 4000 OP_ADD"
            }
          ]
        }
      ]
    },
  }
}

Any of the variables we over-ride in our resolve object (e.g. <player_1_datasig>) will also be added to scripts and prefixed with resolved.:

{
  "scripts": {
    "resolved.player_1_datasig": {
      "script": "$(<player_1_key.schnorr_data_signature.player_1_prediction_raw>)"
    },
  }
}

These resolved variables will also be returned in the payload when CashConnect executes an action (builds the transactions) and are often necessary so that the redeem script can be reconstructed by a Backend/Settlement Service.

If anyone has any input on this (or perhaps a cleaner way of doing it), please let me know. Otherwise, will work on getting this implemented and hopefully have a release with a Stable RPC end of this month/early next month.

I should probably give a brief example of how an action like the above would be executed on the Dapp-side. It would look something like this:

// Construct Cash Connect Dapp instance.
const cashConnect = new CashConnectDapp();

// Connect to WC Relay
await cashConnect.connectToRelay();

// Prompt to establish session with Wallet.
await cashConnect.pair({
      bch: {
        chains: ['bch:bitcoincash'],
        methods: [
          'bch_executeAction'
        ],
        events: ['balancesChanged'],
        template: GuruTemplate,
      },
    }
})

// Execute the bch.create_offer action from the template.
const response = await cashConnect.request<ExecuteAction>('bch_executeAction', {
  "action": "bch.create_offer",
  "data": {
    // Guru Offer Variables
    "expiry_timestamp": 1701344789,
    "player_2_amount": 10000,
    // Guru Game Variables
    "maturity_timestamp": 1701344789,
    "fee_locking_bytecode": "bitcoincash:qzv0p883f0suqjh7z5808xef3jrhukxp8ydzmtz35e",
    "oracle_public_key": "0x02d09db08af1ff4e8453919cc866a4be427d7bfe18f2c05e5444c196fcf6fd2818",
    "player_1_locking_bytecode": "bchtest:qrttgxmvydxt7jlc00jhxafmf4deq2x5es6n2uuy8t",
    "player_1_prediction": 12000
  }
});