replace incorrect article content with correct one

This commit is contained in:
emergent-reasons
2025-10-16 16:18:43 +09:00
parent 8c5a85935b
commit d888982201
@@ -8,6 +8,11 @@ tags: [templates, engine, technical audience]
# Vision to Reality: P2PKH Fungible Token Request
In this article, we are going to go over how a templating engine might function in the context of using a P2PKH template to request fungible tokens.
We will cover both the expected flow for a user interaction, as well as explaining how the engine gets all data it requires to function.
---
```mermaid
mindmap
root )Responsibilities(
@@ -48,17 +53,13 @@ mindmap
🏷️ Lockscripts
```
In this article, we are going to go over how a templating engine might function in the context of a P2PKH template.
We will go over both the expected flow for a user interaction, as well as explaining how the engine gets all data it requires to function.
Finally, we will cover some of the rationale for why this system is valuable.
---
## The Templating System
### Engine
At the center of the templating system is an engine that together with templates is responsible for the full lifecycle of `transactions`, `relevant state` and `private keys`.
At the center of the templating system is an engine that together with templates is responsible for the full lifecycle of `lockingscripts`, `outputs`, `relevant state` and `private keys`.
### Templates
@@ -70,7 +71,7 @@ Templates are organized around actions which defines when and what data to creat
When a template action is requested, the engine follows a consistent and re-usable invitation process.
First an invitation is created that defines what template and action has been requested. This invitation is then accepted by entities wanting to participate in that action, including which of the available roles they want take.
First an invitation is created that defines what template and action has been requested. This invitation is then accepted by entities wanting to participate in that action, including which of the available roles they want take on.
Then, the invitation is extended with any missing information by the participants until it reaches a state where the action defined in the invitation can be executed.
@@ -82,9 +83,7 @@ When we are working with templates and interactions using invitations, it is imp
The engine will deterministically create an identity based on the root entropy provided during initialization. If no entropy is provided it will generate a temporary root entropy and will not have a stable identity.
More information on identities will be covered in a future article.
### Templates
_More information on identities will be covered in a future article._
### Dynamic content
@@ -99,9 +98,9 @@ Here's an example evaluating to `BitcoinCash`, for reference:
> $(<'Bitcoin'> <'Cash'> OP_CAT)
## Detailed explanation of a P2PKH interaction
## Requesting fungible tokens
We will now go over a very detailed example of how the engine, a template and invitations all go together to make a request for `100.00` `MUSD` fungible tokens.
We will now go over a very detailed example of how the engine, a template and invitations all go together to make a request for `100.00` `MUSD` fungible tokens in a way that creates great user experience by keeping the technical complexities confined in the templating system.
### Starting the engine
@@ -149,7 +148,7 @@ start:
},
```
Note that these actions are grouped by role, in this case the P2PKH owner can receive and request funds. Other templates might have different starting actions for different roles.
Note that these actions are grouped by role, in this case the wallet owner can receive and request funds. Other templates might have different starting actions for different roles.
Since we are looking to specifically request `100.00` `MUSD` fungible tokens, we will need to take a look at the `requestFungibleTokens` action:
@@ -169,9 +168,9 @@ The name and description here describes the action itself and is intended to hel
The action will in the end produce a `requestFungibleTokensTransaction` transaction, but to do so it needs some variables like the `requestedTokenCategory` and the `requestedTokenAmount`.
Here's the definition of these variables in the template:
Here's the description of these variables in the template:
```
```ts
requestedTokenCategory:
{
name: 'Requested Token Category',
@@ -221,40 +220,27 @@ In this example, we will use this invitation to coordinate the details of the re
As part of creating the starting invitation, the engine also needs to provide all information that should come from the current user. Since the starting action here was grouped under the `owner` role, it also automatically accepts the invitation in order to take on that role.
The next steps are going to drill down through the template in order to demonstrate how the engine finds the requirements of the action and how it generates the necessary data to satisfy those requirements without any further user interactions.
Here's an overview of the P2PKH template structure:
The next steps are going to drill down through the template in order to demonstrate how the engine finds the requirements of the action and how it generates the necessary data to satisfy those requirements without any further user interaction.
```mermaid
flowchart RL
subgraph Template
Transaction[requestFungibleTokensTransaction]
Output[requestFungibleTokensOutput]
Lock[receivingLockingScript]
Hash[["$(&lt;ownerKey.public_key&gt; OP_HASH160)"]]
Hash --> Lock
Lock --> Output
Output --> Transaction
end
subgraph Engine
Hash[["$(&lt;ownerKey.public_key&gt; OP_HASH160)"]]
Public[ownerKey.publicKey]
Private[ownerKey]
Secret[Seed]
Public --> Hash
Private --> Public
Secret --> Private
end
```
Here is the `requestFungibleTokensTransaction` that the action needs to produce:
We start with the `requestFungibleTokensTransaction` that the action needs to produce:
```ts
requestFungibleTokensTransaction:
{
name: 'Completed request',
description: 'Completed request for $(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_DIV).$(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_MOD) $(<requestedTokenCategory.symbol>).',
name: 'Request',
description: 'Request for $(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_DIV).$(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_MOD) $(<requestedTokenCategory.symbol>).',
icon: 'request',
// Standard transaction without a locktime.
@@ -263,18 +249,18 @@ requestFungibleTokensTransaction:
// Inputs and outputs that must exist in the transaction.
inputs: [ ],
outputs [ 'requestFungibleTokensOutput' ],
outputs: [ 'requestFungibleTokensOutput' ],
// ...
composable: true,
},
```
As you can see, it's a simple transaction construction, with a requirement set for a single `requestFungibleTokensOutput` output.
As you can see, it's a simple transaction construction, with a requirement set for a single `requestFungibleTokensOutput`.
The transaction also has a description that might not look very human readable in here, but once you take the description provided and apply the CashASM evaluations in it, it transforms to a much more human friendly:
The transaction also has a description that might not look very human readable in here, but once you take the description provided and apply the CashASM evaluations in it, it transforms to a much more human friendly version:
> Completed request for 100.00 MUSD.
> Request for 100.00 MUSD.
The engine now needs to determine how to create the `requestFungibleTokensOutput`:
@@ -348,9 +334,9 @@ The engine can now evaluate the CashASM part of the `lockP2PKH` script to create
The `ownerKey` is not added to the invitation though, and we will go into more details when discussing state and secrets later in the article.
_At this point, we have gone through all the requirements for setting up a request to receive `100.00` `MUSD` and we have created an invitation that can be shared to complete the request._
_At this point, we have gone through all the requirements for setting up a request to receive `100.00` `MUSD` and we have created an invitation that can be shared to complete that request._
### Handling the fungible tokens request
## Handling the request
Let's assume that we have received the request invitation above via an XO compatible transport, and that we have an XO compatible wallet with sufficient funds in it.
@@ -363,7 +349,7 @@ In order to handle the request, there is a number of questions that will need to
Ideally, we also want to interact with the wallet user as few times as possible, so the first thing we would need to do is figure out what is required of the user.
#### Roles
### Roles
We start by checking if there is any open roles the user can take:
@@ -384,7 +370,7 @@ const acceptedRequestInvitiation = await xoEngine.acceptInvitation(
_In a more complex setup, this would require showing the user a list of available roles that they can choose from, before accepting the invitation_
#### Variables
### Variables
Next step is to check what variables are missing to complete the request:
@@ -395,11 +381,11 @@ const { requiredVariables } =
This step is needed because the required variables could be used to define the resources needed to be provided later.
The action in this invitation only required two variables, the `requestedTokenAmount` and `requestedTokenCategory`. Both of these were already set by the `owner`, so there's nothing for us to do here.
The action in this invitation only required two variables, the `requestedTokenAmount` and `requestedTokenCategory`. Both of these were already set by the `owner`, so there's nothing for us to do at this point.
_In a more complex setup, this would require asking the user to input values for their required variables before moving on._
_In a more complex setup, this could be handled automaticelly by an app or require asking the user to manually input values for their required variables before moving on._
#### Inputs and outputs
### Inputs and outputs
The last step is to determine what resources are required in terms of blockchain assets.
@@ -409,14 +395,14 @@ For satoshis, we look at the the `requestFungibleTokensOutput` to see `1000`:
For tokens we also look at the `requestFungibleTokensOutput` and here we can see that it has a fungible token amount that was evaluated to `10000n` and a token category of `b38a33f750f84c5c169a6f23cb873e6e79605021585d4f3408789689ed87f366`:
token:
{
category: '$(<requestedTokenCategory>)',
amount: '$(<requestedTokenAmount>)',
nft: null,
},
Now that the engine knows what resources must exist in the output of the transaction, we can infer this as the required resources to provide if we want to complete this action.
```ts
token:
{
category: '$(<requestedTokenCategory>)',
amount: '$(<requestedTokenAmount>)',
nft: null,
},
```
Now that the engine knows what resources must exist in the output of the transaction, it can infer the required inputs. In this case to cover the required output and mining fee.
@@ -435,9 +421,9 @@ const preparedRequestInvitation = await xoEngine.reserveResources(
);
```
In a later example we will show how to do this manually, to achieve coin control, choice in coin selection strategy and using different lockscripts for different change outputs to maintain privacy.
_In a later example we will show how to do this manually, to achieve coin control, choice in coin selection strategy and using different lockscripts for different change outputs to maintain privacy._
#### Authorization
### Authorization
Now that we know all roles, variables and resources the user would provide, we can show this information to the user and ask for a confirmation.
@@ -449,7 +435,7 @@ const signedRequestInvitation = await xoEngine.signInvitation(
);
```
The invitation can then be sent back, or if signed by all relevant parties, we can execute the action in the template:
The invitation can then be sent back, or if signed by all relevant parties, we can execute the action in the template directly:
```ts
const requestTransaction = await xoEngine.executeAction(
@@ -458,247 +444,10 @@ const requestTransaction = await xoEngine.executeAction(
);
```
The request for `100.00` `MUSD` fungible tokens is now complete and a transaction exist on-chain.
_The request for `100.00` `MUSD` fungible tokens is now complete and a transaction exist on-chain._
### Managing state
---
Now that the easy part is done, let's dig into the heart of the engine and the templating system.
While this process may seem more complex than necessary in comparison to ordinary BIP21 requests and using BIP44 to manage keys and lockscripts, it comes with many advantages that improves the user experience and safety of user interactions, while also being capable of working directly with a large number of smart contract systems.
Now that a transaction has been broadcast on chain that has an output matching the locking script we used for our request, the engine will automatically detect this output and internally connect it with the template and action, keep track of the original variables.
However, if you remember from earlier the `ownerKey` was generated on-demand as-needed, but in order to spend this new output we would need the same private key as was used to create the lockscript, so let's revisit the `receivingLockingScript`, this time without shortening it:
```ts
receivingLockingScript:
{
name: 'Received',
description: 'Funds received without wallet coordination.',
icon: 'address',
// Defines how spending future received funds should be authorized.
lockingType: 'standard',
lockingScript: 'lockP2PKH',
// Define a default unlocking script to be used when no action provided script is present.
unlockingScript: 'unlockP2PKH',
roles:
{
owner:
{
// The only state that is required to be persisted when receiving funds is the owners private key.
state: [],
secrets: [ 'ownerKey' ],
// List actions that can be taken with the ownerKey for each address/lockscript.
actions: [ 'sign', 'verify', 'sendSatoshis', 'sendFungibleTokens', 'sendNonfungibleTokens', 'burnFungibleTokens', 'burnNonfungibleTokens' ],
// Indicates how much of received funds should be part of a wallets total balance.
balance:
{
satoshis: true,
fungibleTokens: true,
nonFungibleToken: true,
},
// Indicate when received funds should be considered in automatic coin selection to meet input requirements of other transactions.
selectable: true,
// Indicate that received funds are not considered good privacy.
privacy: false,
},
},
}
}
```
The description here might seem a little odd at first, as it does not match the action we had earlier, but this is intentional.
At any time when the lockscript is generated, it is the engines responsibility to start tracking that lockscript and all activity related to it. When such activity can be linked to an action, it will use the descriptions provided by that action, but sometimes there is activity that the engine is not aware of, such as when you receive an airdrop or someone dusts your address.
The name and description of the lockscript are then used for such outputs that cannot be linked to an intentional action, allowing us to provide human meaningful context even for uncoordinated activity.
You can also see that there is a default unlocking script, which is used when the engine needs to use an output on this lockscript and have not received any explicit instructions on how to unlock it.
The important part for us right now though, is the `state`/`secrets`.
```ts
// The only state that is required to be persisted when receiving funds is the owners private key.
state: [],
secrets: [ 'ownerKey' ],
```
When the engine is asked to create either an output of a lockscript, the template can define a set of variables that are to be automatically managed. The engine would then take this state and internally tie it to the output, or any output using the specific lockscript.
When the output is later spent, the state attached to it will be made available to satisfy any constraints originating from the same template. In this P2PKH example, the `ownerKey` is marked as part of the `secrets` which is a subset of the full state that will not be shared when doing invitations or similar actions.
In more advanced uses, the state might be defined at one transaction, but only be needed several transactions later. In these cases, it is up to the template authors to define outputs to carry the state from one transaction to the next until it is no longer needed.
Note that a result of this behaviour is that state has a lifecycle and once an output is spent, any state attached to that output can be archived into history or even pruned entirely, depending on the needs of the wallet or application.
Managing state is likely to most complex and valuable part of the templating system, and much more can be said on the matter. We will go into more detail in later examples.
### What is ownership?
There are a few more parts of the `receivingLockingScript` left to go over that relates to ownership and control of outputs: `actions`, `balance`, `selectable` and `privacy`.
These properties provide information to an app or wallet that uses the engine. Let's go over each of them to get a better understanding of what they do:
```ts
// List actions that can be taken with the ownerKey for each address/lockscript.
actions: [ 'sign', 'verify', 'sendSatoshis', 'sendFungibleTokens', 'sendNonfungibleTokens', 'burnFungibleTokens', 'burnNonfungibleTokens' ],
```
The `actions` list provides references to actions that you might be able to take with a given output or the state attached to an output.
In the P2PKH template, these can be summarized as actions to sign and verify messages as well as sending and burning value.
This allows an app or wallet to present possible actions when the user is looking at a given output or address. Some of the actions listed here may have additional constraints, for example:
```ts
// Sending is only available for outputs that have fungible tokens on them.
condition: '$(OP_INPUTINDEX OP_UTXOTOKENAMOUNT <0> OP_GREATERTHAN)',
```
The `sendFungibleTokens` action has a condition that makes it unavailable if the input does not have any fungible tokens on it.
Apart from potential `actions`, there is also a `balance` property which can be used to get a better understanding of how much of an output is estimated to owned by the user:
```ts
balance:
{
satoshis: true,
fungibleTokens: true,
nonFungibleToken: true,
},
```
In the P2PKH example, these are set to `true` to indicate that the full output is estimated to be owned by the owner. However, these fields can also be set to `false`, or a number.
In more advanced cases, these fields would be CashASM evaluations that would be able to determine values based on the variables in the state, and in some cases they would be impossible to define and would be unknown.
It is up to the app or wallet to determine how they want to use, or not use, this information.
Finally, there are two properties relating to coin selection: `selectable` and `privacy`:
```ts
// Indicate when received funds should be considered in automatic coin selection.
selectable: true,
// Indicate that received funds are not considered good privacy.
privacy: false,
```
Coin selection is a vast field with no best answer, so these are provided as additional information that a coin selection algorithm may use, and not as strict directives for when and how to do coin selection.
The `selectable` property allows the template authors to indicate if it is sensible to use an output in automatic coin selection and can be either `true` (may be used), `false` (should not be used) or can be left `undefined`.
The `privacy` property allows the template authors to indicate what level of privacy an output has, which can be helpful to avoid mixing of inputs of different levels of privacy.
### Other actions
Before we wrap up, let's take a deeper look at the `sign` and `verify`actions:
```ts
sign:
{
name: 'Sign Message',
description: 'Signs a provided message using the Bitcoin message signing protocol.',
icon: 'sign',
variables: [ 'message' ],
data: [ 'messageSignature' ],
},
verify:
{
name: 'Verify Message Signature',
description: 'Verifies a provided message signature according to the Bitcoin message signing protocol.',
icon: 'verify',
variables: [ 'messageSignature', 'message' ],
data: [ 'messageSignatureValidity' ],
},
```
Contrary to previous actions we have looked at, these do not result in transactions, but instead provides arbitrary `data`.
Let's look at the `messageSignature` definition, and the related `messagePrefix` constant:
```ts
messageSignature:
{
// Evaluate CashASM expression to get the signature needed.
value: '$(<messagePrefix> <message> OP_CAT <ownerKey.data_signature.top_stack_element>)',
type: 'bytes',
hint: 'signature',
},
```
```ts
messagePrefix:
{
name: 'Message Prefix',
description: 'Standard message prefix used in the bitcoin signed message protocol.',
type: 'bytes',
// Value is enforce to the bitcoin signing magic string:
// "\x18Bitcoin Signed Message:\n"
value: '0x18426974636f696e205369676e6564204d6573736167653a0a',
},
```
Together, these encode the standard bitcoin message signing procedure to safely sign an arbitrary message using the `ownerKey` from any given address or output, without exposing the keys to 3rd parties or allowing them to sign potentially dangerous messages.
The validation of a signature is done in a similar way:
```ts
messageSignatureValidity:
{
// Evalute the validity of the message with the owners public key.
value: '$(<messageSignature> <messagePrefix> <message> OP_CAT <ownerKey.publicKey> OP_CHECKDATASIG)';
type: 'integer',
hint: 'script_boolean',
},
```
This means that any wallet that uses this P2PKH template would be have this feature without needing to add any special handling or code for it, simply find any address/lockscript or output, look at the associated actions and initiate an action to sign or verify a message.
## Rationale
After reading this article, you might rightfully feel that it's all a bit much, and wonder why someone would go through the effort and spend the resources required to build such a complicated machinery.
The answer is that it allows us to build new applications both faster and safer, while providing a significantly better user experience.
### Safety
In the traditional wallet and app ecosystem today, the default is to run on HD wallets using seed words for backup and recovery.
With every new feature added to the Bitcoin Cash blockchain that empowers smart contract developers, the number of use cases where the wallet seed is insufficient grows.
Contracts by nature are stateful and loss of state is often equivalent to loss of funds. While this can be mitigated to some degree by carrying state in token commitments, it has the tradeoff of exposing that state to the world. Further, some state, like your prediction in a guru bet, is required to be kept secret, and cannot be safely stored in a token commitment.
By addressing the need for state management in modern smart contracts we reduce the risk of state loss and therefor improve safety for users.
Further, by creating an engine that is capable of both generating and storing keys, and keeping them isolated and restricting their use to template defined actions, we provide additional safety in terms of safe key management. This is also a missing piece in order to achieve fully non-custodial applications, where both control of all keys, as well as decisionmaking for what those keys are used for resides entirely with the users wallet.
Finally, by creating an invitation and coordination mechanic that doesn't rely on single-key operation and that does template based validation of provided data, we improve safety for users when interacting with 3rd party distributed applications.
### User experience
With templates, every transaction, input, output, lockscript and address can be described with dynamically evaluated CashASM names and descriptions. Further, every variable or input required by the user can be described as well, and their type and data can be validated.
With templates, the need for technical identifiers is massively reduced.
Further, by having contextual actions, external links and other metadata it becomes much easier for users to be empowered and stay in control.
Finally, with templates it becomes possible to estimate a users aggregate net worth across a wide variety of contracts, as well as show how much of that value is readily accessible for spending and how much is currently locked.
### Higher productivity
With templates, the correct use of a contract is directly encoded. Importing a template that someone else has made can be done to provide new features at almost no cost.
Compared to some alternatives, templates also provide a consistent way to adopt new features where previously app and wallet makers would need to import specific libraries for each feature they want, or do full bespoke implementations.
For reference, the AnyHedge library, excluding tests and examples, is `262.8 KiB` large.
While it can do much more than a corresponding template, a lot of it is re-inventing the BitcoinCash domain knowledge, working with inputs and outputs etc, and my estimate is that less than a third of it would been needed if we had full template support.
In part 2 of this series, we will cover the remaining parts of the P2PKH template, go over how keys and state is managed, how to estimate ownership in shared outputs and go into more depth on what makes this system valuable.