Field | Description |
---|---|
ZIP | deeZNNutz-0002 |
Title | Hash(ed) Time Lock(ed) Contracts |
Author | georgezgeorgez |
Status | DRAFT |
Type | Spork, Standards |
Created | 2023-02-20 |
License | BSD-2-Clause |
License-Code | GPL v3.0 |
Comments-URI | Big-Inches-Club-House/bich#1 |
Hash(ed) Time Lock(ed) Contracts allow for contingent payments based on knowledge of the preimage of a hash. If locked funds are not unlocked by a specified expiration time (the timelock), the creator of the HTLC can then reclaim the funds.
In this ZIP we propose supporting HTLCs through a new spork-activated embedded contract with an additional corresponding json-rpc api endpoint.
We also implement an account-level toggle-able feature called ProxyUnlock, where other users can unlock HTLCs on behalf of the hashlocked user while the funds are still sent to the hashlocked address. We propose the feature to be enabled by default.
Embedded contract address: z1qxemdeddedxhtlcxxxxxxxxxxxxxxxxxygecvw
Create
creates a new HTLC and gives it the id of the account block that created it. A user can send any non-zero amount of any ZTS token. Pillars cannot confirm a Create transaction that results in an htlc that is already expired. The current reference implementation only supports a hashType of 0
for SHA3-256 or 1
for SHA2-256, but the contract is designed to be able to support more hash functions in the future. The digest size of the hashLock must match the chosen hashType. The method costs the standard "Embedded Simple" plasma amount of 2.5 * 21000.
JSON ABI:
{"type":"function","name":"Create", "inputs":[
{"name":"hashLocked","type":"address"},
{"name":"expirationTime","type":"int64"},
{"name":"hashType","type":"uint8"},
{"name":"keyMaxSize","type":"uint8"},
{"name":"hashLock","type":"bytes"}
]}
Reclaim
specifies an HTLC id can only be called (and confirmed by a Pillar) by the creator of an HTLC after it has expired. An HTLC is expired if the timestamp of the confirming momentum is greater or equal to the specified expirationTime. It returns the ZTS token amount that was sent during the Create method call. The method costs the standard "Embedded Withdraw" plasma amount of 3.5 * 21000.
JSON ABI:
{"type":"function","name":"Reclaim","inputs":[
{"name":"id","type":"hash"}
]}
Unlock
specifies an HTLC id and a preimage that unlocks that HTLC's hashlock. The method can only be called (and confirmed by a Pillar) before the HTLC expires. An HTLC is expired if the timestamp of the confirming momentum is greater or equal to the HTLC's expirationTime.
The preimage must hash to the hashlock using the hash function specified by the HTLC's hashType. The byte size of provided preimage must also be less than or equal to the HTLC's keyMaxSize.
If the HTLC's hashlocked user's ProxyUnlock setting has been disabled, only the hashlocked user may call the Unlock method. Otherwise anyone may call the method for an HTLC.
If all conditions are met, the ZTS token amount that was sent during the Create method call is sent to the HTLC's specified hashLocked address. The method costs the standard "Embedded Withdraw" plasma amount of 3.5 * 21000.
JSON ABI:
{"type":"function","name":"Unlock","inputs":[
{"name":"id","type":"hash"},
{"name":"preimage","type":"bytes"}
]}
DenyProxyUnlock takes no arguments and disables the Proxy Unlock feature for the account that calls it. The method costs the standard "Embedded Simple" plasma amount of 2.5 * 21000
JSON ABI:
{"type":"function","name":"DenyProxyUnlock","inputs":[]}
AllowProxyUnlock takes no arguments and enables the Proxy Unlock feature for the account that calls it. The method costs the standard "Embedded Simple" plasma amount of 2.5 * 21000
JSON ABI:
{"type":"function","name":"AllowProxyUnlock","inputs":[]}
RPC endpoint: embedded.htlc
Request params: takes in a single Hash id
{
"type": "array",
"items": [
{
"type": "string"
}
]
}
Response results: returns an HtlcInfo object
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"timeLocked": {
"type": "string"
},
"hashLocked": {
"type": "string"
},
"tokenStandard": {
"type": "string"
},
"amount": {
"type": "integer"
},
"expirationTime": {
"type": "integer"
},
"hashType": {
"type": "integer"
},
"keyMaxSize": {
"type": "integer"
},
"hashLock": {
"type": "string"
}
},
"required": [
"id",
"timeLocked",
"hashLocked",
"tokenStandard",
"amount",
"expirationTime",
"hashType",
"keyMaxSize",
"hashLock"
]
}
Request params: takes in a single address
{
"type": "array",
"items": [
{
"type": "string"
}
]
}
Response results: returns a boolean indicating if the account's Proxy Unlock feature is enabled
{
"type": "boolean",
}
Our primary motivation for HTLCs is to enable contingent payments for use cases such as atomic swaps which can span across multiple networks. This was something explicitly mentioned as part of the HyperCore interoperability initiative. These use cases can provide the network with new sources of decentralized liquidity.
Although HTLCs don't provide any native protocol incentives, we currently don't have a general smart contract runtime. HTLCs are a simple interface and don't require any looping constructs which would complicate plasma costs. The feature also can help greatly with bringing liquidity to the protocol, so we propose to support HTLCs sooner rather than later.
Several other embedded contracts have "revoke" methods, indicating an action which invalidates an entry and returns funds. For htlcs, we invalidate unlocking via preimage as soon as soon as the expiration time arrives, however the funds still sit in the contract and exist as an entry, so we use "reclaim".
Instead of having a single method UpdateProxyUnlockStatus which takes in a boolean, we use two contract methods DenyProxyUnlock and AllowProxyUnlock with no parameters because they are shorter and better express the toggle-able nature of the feature.
To prevent against key size attacks, we require HTLC creators to specify a keyMaxSize. Randomly generated 32 byte preimages seem to be the standard, and we use a uint8 to future proof for larger preimages.
Most applications of HTLCs, use a shared hashlock for atomicity between transactions. These use cases require parties to monitor a network for publicly posted hashlock preimages and make corresponding transactions before timelocks expire.
The proxy unlock feature effectively allows other network participants to help enforce the atomicity of transactions such as atomic swaps. NoM's feeless properties take this from a possibility to a plausibility.
The feature is opinionatedly enabled by default because:
- transaction atomicity seems to be the primary/only use case of HTLCs
- it does not impose additional burdens on any user involved
- the only loss is unlock optionality, but funds can always be sent back
- removes a step for the average user to use HTLCs with increased safety that they may otherwise not take
We specify hashLock as byte Array rather than a Hash because Hash has a size of 32 bytes and is meant to work with Zenon's standard SHA3-256 digests. Although a 32 byte digest is standard at the moment, in the future we may want to support hashlocks with other digest sizes.
As none of the contract methods require any sort of looping mechanism, we are consistent with the plasma costs of other embedded contracts.
Methods that don't result in additional transactions use the standard "Embedded Simple" plasma costs. While methods that result in a withdrawal (Reclaim, Unlock) use the standard "Embedded Withdraw plasma costs.
Because HTLCs can be withdrawn by two users, the creator and the hashLocked user, there is no natural way to store them in a way that is iterable by address without additional indexing that is non-essential to the Create, Reclaim/Unlock flow. As such these methods are not included in this proposal, but may feature in future proposals around pruna ble indexes.
A reference implementation for go-zenon which implements HTLCs for NoM's native SHA3-256 and the widespread SHA2-256 is available at: https://github.com/Big-Inches-Club-House/go-zenon/tree/htlc