-
Notifications
You must be signed in to change notification settings - Fork 410
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Blinded commitment verification (#276)
* Initial implementation of blinded commitment verification Refs #274 * Don't mix up query string and hash parameters A fallback from query string parameters to hash parameters was implemented to retain backwards compatibility with old URLs. This was causing the unblinding data to leak into the query string whenever the query string was updated, e.g. by expanding details. Removing the compatibility is not terrible, because the old style URLs were only used for a short while, and this just means that auto-expand and highlighting of specific inputs/outputs won't work. * Retain hash parameters when updating the query string * Don't pollute the global namespace with Module/getValue/ccall/etc This requires running the emscripten compiler with `-s MODULARIZE=1 -s EXPORT_NAME=InitWally`. * Add libwally WASM to the Docker build process * Switch to upstream libwally * Fix WASM_URL path * Fix rendering of coinbase transactions * Display a warning if any of the blinders do not match
- Loading branch information
Showing
14 changed files
with
292 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,3 +13,4 @@ data_liquid_mainnet | |
data_bitcoin_testnet | ||
data_bitcoin_regtest | ||
data_liquid_regtest | ||
www/libwally |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { Observable as O } from '../rxjs' | ||
import * as libwally from '../lib/libwally' | ||
|
||
// Accepts a stream of blinding data strings, returns a stream of Unblinded | ||
// objects with a map from the commitments to the unblinded data | ||
module.exports = blinders_str$ => | ||
O.from(blinders_str$).flatMap(async blinders_str => { | ||
if (!blinders_str) return null | ||
|
||
await libwally.load() | ||
|
||
try { | ||
const blinders = parseBlinders(blinders_str) | ||
return new Unblinded(blinders) | ||
} | ||
catch (error) { | ||
return { error } | ||
} | ||
}) | ||
.share() | ||
|
||
class Unblinded { | ||
constructor(blinders) { | ||
this.commitments = makeCommitmentMap(blinders) | ||
} | ||
|
||
// Look for the given output, returning an { value, asset } object | ||
find(vout) { | ||
return vout.assetcommitment && vout.valuecommitment && | ||
this.commitments.get(`${vout.assetcommitment}:${vout.valuecommitment}`) | ||
} | ||
|
||
// Lookup all transaction inputs/outputs and attach the unblinded data | ||
tryUnblindTx(tx) { | ||
if (tx._unblinded) return tx._unblinded | ||
let matched = 0 | ||
tx.vout.forEach(vout => matched += +this.tryUnblindOut(vout)) | ||
tx.vin.filter(vin => vin.prevout).forEach(vin => matched += +this.tryUnblindOut(vin.prevout)) | ||
tx._unblinded = { matched, total: this.commitments.size } | ||
return tx._unblinded | ||
} | ||
|
||
// Look the given output and attach the unblinded data | ||
tryUnblindOut(vout) { | ||
const unblinded = this.find(vout) | ||
if (unblinded) Object.assign(vout, unblinded) | ||
return !!unblinded | ||
} | ||
} | ||
|
||
function makeCommitmentMap(blinders) { | ||
const commitments = new Map | ||
|
||
blinders.forEach(b => { | ||
const { asset_commitment, value_commitment } = | ||
libwally.generate_commitments(b.value, b.asset, b.value_blinder, b.asset_blinder) | ||
|
||
commitments.set(`${asset_commitment}:${value_commitment}`, { | ||
asset: b.asset, | ||
value: b.value, | ||
}) | ||
}) | ||
|
||
return commitments | ||
} | ||
|
||
// Parse the blinders data from a string encoded as a comma separated list, in the following format: | ||
// <value_in_satoshis>,<asset_tag_hex>,<amount_blinder_hex>,<asset_blinder_hex> | ||
// This can be repeated with a comma separator to specify blinders for multiple outputs. | ||
|
||
function parseBlinders(str) { | ||
const parts = str.split(',') | ||
, blinders = [] | ||
|
||
while (parts.length) { | ||
blinders.push({ | ||
value: verifyNum(parts.shift()) | ||
, asset: verifyHex32(parts.shift()) | ||
, value_blinder: verifyHex32(parts.shift()) | ||
, asset_blinder: verifyHex32(parts.shift()) | ||
}) | ||
} | ||
return blinders | ||
} | ||
|
||
function verifyNum(num) { | ||
if (!+num) throw new Error('Invalid blinding data (invalid number)') | ||
return +num | ||
} | ||
function verifyHex32(str) { | ||
if (!str || !/^[0-9a-f]{64}$/i.test(str)) throw new Error('Invalid blinding data (invalid hex)') | ||
return str | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
const WALLY_OK = 0 | ||
, ASSET_COMMITMENT_LEN = 33 | ||
, ASSET_GENERATOR_LEN = 33 | ||
, ASSET_TAG_LEN = 32 | ||
, BLINDING_FACTOR_LEN = 32 | ||
|
||
const STATIC_ROOT = process.env.STATIC_ROOT || '' | ||
, WASM_URL = process.env.LIBWALLY_WASM_URL || `${STATIC_ROOT}libwally/wallycore.js` | ||
|
||
let load_promise, Module | ||
export function load() { | ||
return load_promise || (load_promise = new Promise((resolve, reject) => { | ||
const script = document.createElement('script') | ||
script.src = WASM_URL | ||
script.addEventListener('error', reject) | ||
script.addEventListener('load', () => | ||
InitWally().then(module => { Module=module; resolve() }, reject)) | ||
document.body.appendChild(script) | ||
})) | ||
} | ||
|
||
// Simple wrapper to execute both asset_generator_from_bytes and asset_value_commitment, | ||
// with hex conversions | ||
export function generate_commitments(value, asset_hex, value_blinder_hex, asset_blinder_hex) { | ||
const asset = parseHex(asset_hex, ASSET_TAG_LEN) | ||
, value_blinder = parseHex(value_blinder_hex, BLINDING_FACTOR_LEN) | ||
, asset_blinder = parseHex(asset_blinder_hex, BLINDING_FACTOR_LEN) | ||
|
||
const asset_commitment = asset_generator_from_bytes(asset, asset_blinder) | ||
, value_commitment = asset_value_commitment(value, value_blinder, asset_commitment) | ||
|
||
return { asset_commitment: encodeHex(asset_commitment) | ||
, value_commitment: encodeHex(value_commitment) } | ||
} | ||
|
||
export function asset_generator_from_bytes(asset, asset_blinder) { | ||
const asset_commitment_ptr = Module._malloc(ASSET_GENERATOR_LEN) | ||
checkCode(Module.ccall('wally_asset_generator_from_bytes' | ||
, 'number' | ||
, [ 'array', 'number', 'array', 'number', 'number', 'number' ] | ||
, [ asset, asset.length | ||
, asset_blinder, asset_blinder.length | ||
, asset_commitment_ptr, ASSET_GENERATOR_LEN | ||
])) | ||
|
||
const asset_commitment = readBytes(asset_commitment_ptr, ASSET_GENERATOR_LEN) | ||
Module._free(asset_commitment_ptr) | ||
return asset_commitment | ||
} | ||
|
||
export function asset_value_commitment(value, value_blinder, asset_commitment) { | ||
// Emscripten transforms int64 function arguments into two int32 arguments, see: | ||
// https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-pass-int64-t-and-uint64-t-values-from-js-into-wasm-functions | ||
const [value_lo, value_hi] = split_int52_lo_hi(value) | ||
|
||
const value_commitment_ptr = Module._malloc(ASSET_COMMITMENT_LEN) | ||
checkCode(Module.ccall('wally_asset_value_commitment' | ||
, 'number' | ||
, [ 'number', 'number', 'array', 'number', 'array', 'number', 'number', 'number' ] | ||
, [ value_lo, value_hi | ||
, value_blinder, value_blinder.length | ||
, asset_commitment, asset_commitment.length | ||
, value_commitment_ptr, ASSET_COMMITMENT_LEN | ||
])) | ||
|
||
const value_commitment = readBytes(value_commitment_ptr, ASSET_COMMITMENT_LEN) | ||
Module._free(value_commitment_ptr) | ||
return value_commitment | ||
} | ||
|
||
function checkCode(code) { | ||
if (code != WALLY_OK) | ||
throw new Error(`libwally failed with code ${code}`) | ||
} | ||
|
||
function readBytes(ptr, size) { | ||
const bytes = new Uint8Array(size) | ||
for (let i=0; i<size; i++) bytes[i] = Module.getValue(ptr+i, 'i8') | ||
return bytes | ||
} | ||
|
||
// Split a 52-bit JavaScript number into two 32-bits numbers for the low and high bits | ||
// https://stackoverflow.com/a/19274574 | ||
function split_int52_lo_hi(i) { | ||
let lo = i | 0 | ||
if (lo < 0) lo += 4294967296 | ||
|
||
let hi = i - lo | ||
hi /= 4294967296 | ||
|
||
if ((hi < 0) || (hi >= 1048576)) throw new Error ("not an int52: "+i) | ||
|
||
return [ lo, hi ] | ||
} | ||
|
||
function encodeHex(bytes) { | ||
return Buffer.from(bytes).toString('hex') | ||
//return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('') | ||
} | ||
|
||
// Parse hex string encoded in *reverse* | ||
function parseHex(str, expected_size) { | ||
if (!/^([0-9a-f]{2})+$/.test(str)) throw new Error('Invalid blinders (invalid hex)') | ||
if (str.length != expected_size*2) throw new Error('Invalid blinders (invalid length)') | ||
return new Uint8Array(str.match(/.{2}/g).map(hex_byte => parseInt(hex_byte, 16)).reverse()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.