Skip to content

Instantly share code, notes, and snippets.

@fiatjaf fiatjaf/README.md
Last active Apr 27, 2019

Embed
What would you like to do?
etleneum htlc contract

A contract that can handle multiple "HTLCs" -- in the sense that these are just values parked at the contract that can be claimed by anyone with the correct preimage during some time. After the expiration time they can then be claimed by another different preimage.


Interface


  • method: open
  • satoshis: the number of satoshis sent is the total that will be locked
  • returns: {expires, preimage, withdraw_preimage}
  • payload:
{
  "hash": <a string representing the hex-encoded sha256 hash of a preimage you control so you can use it later> -- optional, if not given a random preimage and hash will be generated and returned back to caller
  "timelock": <a number of seconds> -- optional, if not given the timelock will be 5 days
  "withdraw_hash" <same as "hash" above, this will be only usable after the timelock has expired> -- optional, as "hash" above
}

  • method: claim
  • satoshis: 0
  • returns: {ok, amount, hash}
  • payload:
{
  "hash": <a string that identifies the HTLC>
  "preimage": <preimage used to claim> -- if the HTLC is expired this should be the preimage corresponding to the "withdraw_hash", otherwise corresponding to the "hash"
  "_invoice": <bolt11 invoice to be paid with the full amount of funds in the HTLC>
}
function __init__ ()
return {}
end
function open ()
local preimage
local hash = payload.hash
if hash == nil then
math.randomseed(os.time() + satoshis)
preimage = tostring(math.random())
hash = util.sha256(preimage)
end
local withdraw_preimage
local withdraw_hash = payload.withdraw_hash
if hash == nil then
math.randomseed(os.time() + satoshis)
withdraw_preimage = tostring(math.random())
withdraw_hash = util.sha256(withdraw_preimage)
end
local expiration = os.time() + (payload.timelock or 86400 * 5)
if state[hash] then
error(hash .. ' already exists')
end
state[hash] = {
amount=satoshis,
expiration=expiration,
withdraw_hash=withdraw_hash
}
return {expires=expiration, preimage=preimage, withdraw_preimage=withdraw_preimage}
end
function claim ()
local hash = payload.hash
local preimage = payload.preimage
local htlc = state[hash]
if htlc == nil then
error(hash .. " doesn't exist")
end
local pay = false
if htlc.expiration > os.time() then
-- not expired
if util.sha256(preimage) == hash then
pay = hash
end
else
-- expired
if util.sha256(preimage) == htlc.withdraw_hash then
pay = htlc.withdraw_hash
end
end
if pay then
local _, err = ln.pay(payload._invoice, {exact=htlc.amount})
if err ~= nil then
error("failed to pay: " .. err)
end
state[hash] = nil
return {ok=true, amount=htlc.amount, hash=pay}
else
return {ok=false}
end
end
import uuid
import time
import json
import hashlib
aliases['run'] = $GOPATH[0] + '/src/github.com/fiatjaf/etleneum/runcall'
def make_hash():
preimage = str(uuid.uuid4())
hash = hashlib.sha256(preimage.encode('ascii')).hexdigest()
return preimage, hash
jsonstate = 'null'
state = None
def call(method, payload={}, satoshis=0, funds=0):
global state, jsonstate
r = !(run --contract contract.lua --method @(method) --payload @(json.dumps(payload)) --state @(jsonstate) --satoshis @(satoshis) --funds @(funds))
if not r:
print('')
raise Exception(r.output)
jsonstate = $(echo @(r.output) | jq .State)
print('state after: ' + jsonstate)
state = json.loads(jsonstate)
return json.loads($(echo @(r.output) | jq .ReturnedValue))
withdraw_toomuch = 'lnbc10u1pw8hwmzpp5awlp7xqyyknc3yfmnhg59sk4hfexmfpemrx2m4hrg7k3pdmfmwysdq2w9mk2am3v5xqxae4j0lcqp2rzjqtqkejjy2c44jrwj08y5ygqtmn8af7vscwnflttzpsgw7tuz9r407zyusgqq44sqqqqqqqqqqqqqqqgqpckcnfy2j42leleand2hseqv4zgsc06upmmn6sj509nrahvcdmnkyjvn39785cks97mcnqvatmag4l2hpxdvke0jhgq798m85jmkdrntspg4rm2z'
withdraw_10 = 'lnbc100n1pw8hw3upp552llrqm5274emxpkjyguxtuyxgxwcs8n75sc87fax6mv0asnptksdq2w9mk2am3v5xqxae4j0lcqp2rzjqtqkejjy2c44jrwj08y5ygqtmn8af7vscwnflttzpsgw7tuz9r407zyusgqq44sqqqqqqqqqqqqqqqgqpc5dhs4tmlgmc9z7aw4d236y3c24qd48zdkyk2hse75h4ef28lmaw53dx560d8ddy8503xp0zr8xmwc4fehcf0m55mgf6hlkczca5h7mgpru0jma'
p1, h1 = make_hash()
p2, h2 = make_hash()
p3, h3 = make_hash()
print('init')
call('__init__')
print('open with given preimage')
exp1 = int(time.time()) + 86400 * 5
call('open', {'hash': h1, 'withdraw_hash': h2}, 10)
assert state[h1] == {'withdraw_hash': h2, 'amount': 10, 'expiration': exp1}
print('open another with small expiration time')
exp2 = int(time.time()) + 1
call('open', {'hash': h2, 'withdraw_hash': h3, 'timelock': 1}, 10)
assert state[h2] == {'withdraw_hash': h3, 'amount': 10, 'expiration': exp2}
print('overwrite fail')
exc = False
try:
call('open', {'hash': h2, 'withdraw_hash': h3}, 6547)
except:
exc = True
assert exc
exc = False
try:
call('open', {'hash': h2, 'withdraw_hash': h3}, 6547)
except:
exc = True
assert exc
assert state[h1] == {'withdraw_hash': h2, 'amount': 10, 'expiration': exp1}
assert state[h2] == {'withdraw_hash': h3, 'amount': 10, 'expiration': exp2}
time.sleep(1)
print('claim fail due to wrong hash')
res = call('claim', {'hash': h1, 'preimage': p2}, 0, funds=99999) # not expired
assert res['ok'] == False
res = call('claim', {'hash': h2, 'preimage': p2}, 0, funds=99999) # expired
assert res['ok'] == False
assert state[h1] == {'withdraw_hash': h2, 'amount': 10, 'expiration': exp1}
print('claim fail due to invalid invoice')
exc = False
try:
call('claim', {'hash': h1, 'preimage': p1, '_invoice': withdraw_toomuch}, 0, funds=99999)
except:
exc = True
assert exc
assert state[h1] == {'withdraw_hash': h2, 'amount': 10, 'expiration': exp1}
print('claim success')
res = call('claim', {'hash': h1, 'preimage': p1, '_invoice': withdraw_10}, 0, funds=99999)
assert res['ok'] == True
assert res['hash'] == h1
assert res['amount'] == 10
assert len(state) == 1
call('claim', {'hash': h2, 'preimage': p3, '_invoice': withdraw_10}, 0, funds=99999)
assert state == {}
print('tests passed')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.