Created
October 28, 2017 14:35
-
-
Save christomoore/0094ff5808a43c51b2b254d899f858f8 to your computer and use it in GitHub Desktop.
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
Hi all, | |
An obvious privacy limitation with lightning is that even with onion | |
routing, differents hops can be associated as being part of the same | |
transaction due to sharing a common R value. So if you see a HTLC from | |
Alice to Bob, paying $5 to Bob on receipt of R where SHA(R)=12345..; | |
and you see another HTLC from Carol to Dave, paying $4.95 to Bob on | |
receipt of R under the same condition, SHA(R)=12345..., then you know | |
it's part of the same transaction. | |
If you could change R at each step in the route, this would go away, | |
improving payment anonymity and making it harder to attack the system in | |
general. | |
That's hard, because as a forwarding node, if you receive a HTLC payable | |
on R1, then to send a HTLC payable on R2, you need to be able to | |
calculate R1 from R2 or you'll be out of pocket. But you also can't be | |
able to calculate R1 *without* R2, or you could just rip off whoever's | |
making the payment. And, of course you have to know SHA(R2) to forward the | |
payment at all. And if you only know SHA(R1) and SHA(R2) it's hard to | |
say anything at all about R1 and R2 because cryptographic hash functions | |
are designed to make any structural relationships go away. | |
BUT! I think the magic of SNARKs [0] lets you do this! | |
With a SNARK, you can "prove" that you have some secrets (ie, R1 and R2) | |
that satisfy some programmable condition (ie, SHA(R1)=H1 and SHA(R2)=H2 | |
and R1=R2 XOR X), based on public inputs (H1, H2 and X), without revealing | |
those secrets. | |
I think that's pretty safe, because if you receive an HTLC asking for a | |
preimage for H1, along with instructions in the onion saying ask Bob for | |
a preimage for H2, and here's X and a proof, then either: | |
- your forwarded HTLC will fail, and everything's fine | |
- you'll receive R2, calculate R1=R2 XOR X and see SHA(R1)=H1 as | |
expected, and everything's fine | |
- you'll receive R2, calculate R1=R2 XOR X and see SHA(R1) != H1, | |
which is only possible if the cryptography behind SNARKs are broken | |
- you'll receive RX, such that H2=SHA(RX) but RX being too | |
long or too short. If SNARKs aren't broken, this means that you know | |
R2alt and someone else knows R2 that are different but hash to the | |
same value, meaning SHA has been broken. | |
It seems like there are research-level tools out there that actually | |
make this practical to try out. I've had a go at implementing this using | |
snarkfront [1]. Using it looks like: | |
1) initial setup of proof/verification keys | |
$ ./test_lightning -m keygen > keygen.txt # global setup | |
2) generate a proof, using a 32 byte secret, and XOR key (64 hex digits) | |
$ SECRET="the quick brown fox jumps lazily" | |
$ XOR=$(echo "she sells sea shells" | sha256sum | head -c64) | |
$ cat keygen.txt | | |
./test_lightning -m proof -s "$SECRET" -x "$XOR" > proof.txt | |
m: proof. | |
f: . | |
b: . | |
x: 5677d356ccd6ff29b296a697791d109a1703557ecbbcf52b4f38d4b680858912. | |
F: 74686520717569636b2062726f776e20666f78206a756d7073206c617a696c79 | |
B: 221fb676bda3964ad9b6c4e5166a7eba716c2d5ea1c9985b3c18b8d7faece56b | |
#F: ae4d48f71fdb6f74149fab591e88f2cc07d4e696968def1aa7ca1e07096c5b85 | |
#B: 166e4f9b8ec5895e870f0f0508327d73ba9ad9af4e9841599bafb1bf55c8a245 | |
generate proof | |
(6) .................................................. | |
(5) .................................................. | |
(4) .................................................. | |
(3) .................................................. | |
(2) .................................................. | |
(1) .................................................. | |
3) Verify the proof: | |
$ F=ae4d48f71fdb6f74149fab591e88f2cc07d4e696968def1aa7ca1e07096c5b85 | |
$ B=166e4f9b8ec5895e870f0f0508327d73ba9ad9af4e9841599bafb1bf55c8a245 | |
$ cat keygen.txt proof.txt | | |
./test_lightning -m verify -h "$F" -b "$B" -x "$XOR" | |
m: verify. | |
f: ae4d48f71fdb6f74149fab591e88f2cc07d4e696968def1aa7ca1e07096c5b85. | |
b: 166e4f9b8ec5895e870f0f0508327d73ba9ad9af4e9841599bafb1bf55c8a245. | |
x: 5677d356ccd6ff29b296a697791d109a1703557ecbbcf52b4f38d4b680858912. | |
verify proof (6) (5) (4) (3) (2) (1) | |
proof is verified | |
4) Verify it doesn't report a valid proof with different inputs: | |
$ cat keygen.txt proof.txt | | |
./test_lightning -m verify -h "$B" -b "$F" -x "$XOR" | |
m: verify. | |
f: 166e4f9b8ec5895e870f0f0508327d73ba9ad9af4e9841599bafb1bf55c8a245. | |
b: ae4d48f71fdb6f74149fab591e88f2cc07d4e696968def1aa7ca1e07096c5b85. | |
x: 5677d356ccd6ff29b296a697791d109a1703557ecbbcf52b4f38d4b680858912. | |
verify proof (6) (5) (4) (3) (2) | |
proof is rejected | |
Some results: | |
* the proof/verification key data take up about 100MB -- in theory | |
one set of this data can be used by everyone; the only catch is | |
that everyone has to trust that nobody has kept the original random | |
numbers used to generate it. | |
* proof/verification key data takes about a minute to generate, | |
and about 650MB of RAM. | |
* the proof data itself (which would need to be sent to the node that's | |
going to switch R's) is just 864 bytes; so it'd use up about 5 hops | |
worth of onion routing at 192B per hop -- in a 4096 byte packet eg, | |
you could have four hops, changing R each time; or you could have 9 | |
hops, changing R only three times. | |
* generating the proof data for a given R1,X pair takes about 10 | |
seconds, and 260MB of RAM | |
* verifying the proof is quick-ish -- it takes 0.5s on my laptop, | |
and uses about 150MB of RAM. | |
For comparison, that last point makes a SNARK verification 500+ times | |
more expensive than an ECDH operation. If I got my maths right, you | |
can translate 3c for a linode CPU-hour into 2.5 satoshi for a linode | |
CPU-second (at $338/BTC), so you're probably looking at a minimum fee | |
of a few satoshi per SNARK verification, but that's still pretty okay | |
for transactions of 500 satoshi or more, ie anything more than about a | |
fifth of a US cent. | |
The 10s proof generation time is probably more of a limitation -- though | |
you could generate them in advance easily enough and just store them until | |
you need to use them, which would avoid lag being a problem at least. But | |
even then it's still essentially adding up to 30c of additional costs to | |
your transaction (ie 10s cpu time valued at up to 3c/s), which probably | |
isn't worthwhile for transactions smaller than a dollar or two. | |
A drawback is that you'd either (a) have to do all this on the merchant's | |
side (not just sending SHA(R) to whoever wants to pay you, but sending | |
SHA(R1), SHA(R2), SHA(R3), SHA(R4), X12, X23, X34, and three proofs, | |
which would be pretty painful; or (b) you'd have to generate all the | |
R secrets as a consumer, and you wouldn't get to use the fact that you | |
know R as evidence that you paid the merchant. | |
Anyway, it's obviously not ready for prime time today: SNARKs are still | |
pretty new as a concept; I'm definitely not familiar enough with SNARK | |
theory to be sure I'm not misusing the concept somehow; snarkfront may not | |
have implemented the theory fully correctly; and I might not have captured | |
everything I needed to in order for my "proof" to actually say what I | |
want it to. So not a great idea to use this to protect real money today. | |
But still, this seems like it's not all /that/ far from being practical, | |
and if the crypto's not fundamentally broken, seems like it goes a long | |
way to filling in the biggest privacy hole in lightning today [3]... | |
Code is at https://github.com/ajtowns/snarkfront/ or more directly at: | |
https://github.com/ajtowns/snarkfront/blob/lightning-sha/test_lightning.cpp | |
Cheers, | |
aj | |
[0] https://tahoe-lafs.org/trac/tahoe-lafs/wiki/SNARKs | |
[1] https://github.com/jancarlsson/snarkfront | |
[2] This would also improve privacy/anonymity for other applications of | |
HTLCs, such as atomic swaps across chains: | |
1 bitcoin, payable on R1 + Alice's sig or timeout + Bob's sig | |
100 litecoin, payable on R2 + Robert's sig or timeout + Ally's sig | |
Alice and Bob communicate privately, agreeing to trade 1 BTC for 100 | |
litecoin and revealing their aliases Robert and Ally; Alice generates | |
R1, R2, and reveals SHA(R1), SHA(R2), R1^R2 and the SNARK proof and | |
publishes the litecoin payment. Bob verifies the proof, and publishes | |
the bitcoin payment. Alice claims the bitcoin payment, revealing R1; | |
Bob calculates R2 and claims the litecoin payment. The swap can take | |
place trustlessly because Bob knows the only way Alice can claim his | |
bitcoin is by revealing enough info so he can claim the corresponding | |
litecoin. But there isn't any on-chain information linking the two | |
transactions, because R1 and R2 are independent (and could even be | |
using different hash functions as well as different preimages). | |
After the funds have been claimed, the private communication is | |
also completely deniable, since anyone could generate R1^R2 and a | |
corresponding SNARK proof just using the info on the blockchain. | |
//------------------------------------------------------------------------// | |
https://github.com/ajtowns/snarkfront/blob/lightning-sha/test_lightning.cpp | |
#include <array> | |
#include <cstdint> | |
#include <cstdlib> | |
#include <iostream> | |
#include <string> | |
#include <vector> | |
#include "snarkfront.hpp" | |
using namespace snarkfront; | |
using namespace cryptl; | |
using namespace std; | |
void printUsage(const char* exeName) { | |
cout << "usage: " << exeName | |
<< " -m keygen|input|proof|verify" | |
<< " -s secret" | |
<< " -h hash-of-secret" | |
<< " -b hash-of-secret_xor_delta" | |
<< " -x delta" | |
<< endl; | |
exit(EXIT_FAILURE); | |
} | |
#if (CHAR_BIT != 8) | |
#error "large chars confuses me (CHAR_BIT != 8)" | |
#endif | |
template<typename T> | |
typename T::DigType do_digest( | |
T hashAlgo, | |
std::size_t n, | |
const vector<uint8_t> &data, | |
vector<typename T::WordType> &blessdata) | |
{ | |
typename T::MsgType msg; // 512 bit block as array of 16 uint32_t's, except snarked | |
typename T::WordType::ValueType msg_int; // uint32_t | |
assert(n % sizeof(msg_int) == 0); | |
assert(n < msg.size() * sizeof(msg_int)); // n+padding actually has to equal this | |
assert(blessdata.size() == 0); | |
stringstream ss; | |
std::size_t i = 0; | |
for (const auto& c : data) { | |
ss.put(c); | |
i++; | |
} | |
while (i++ < n) { | |
ss.put('\0'); | |
} | |
bool padded = false; | |
for (auto &m : msg) { | |
if (!bless(msg_int, ss)) { | |
if (!padded && ss.eof()) { | |
std::size_t lengthBits = n * CHAR_BIT; | |
ss.clear(); | |
T::padMessage(ss, lengthBits); | |
assert(lengthBits == msg.size() * sizeof(msg_int) * CHAR_BIT); | |
assert(!ss.eof()); | |
padded = true; | |
} | |
if (!bless(msg_int, ss)) { | |
assert(padded); // should be padded by now | |
assert(false); // ran out of input -- padding didn't work | |
msg_int = ~0; | |
} | |
} | |
bless(m, msg_int); | |
// ensure proof validates correct padding | |
if (padded) | |
assert_true(m == msg_int); | |
else | |
blessdata.push_back(m); | |
} | |
hashAlgo.msgInput(msg); | |
hashAlgo.computeHash(); | |
return hashAlgo.digest(); | |
} | |
typedef BN128_FR FR; | |
typedef BN128_PAIRING PAIRING; | |
template <typename HC, typename HS, std::size_t N> | |
struct Algo { | |
// secret/witness | |
vector<uint8_t> preImageFwd; // digest() only works on vectors | |
vector<uint8_t> preImageBck; | |
// actual input data (hash and xor) | |
typename HC::DigType pubHashFwd; | |
typename HC::DigType pubHashBck; | |
array<typename HC::WordType, N/sizeof(typename HC::WordType)> pubXor; | |
// pub input for circuit (hash and xor) | |
typename HS::DigType pubVarsFwd; | |
typename HS::DigType pubVarsBck; | |
array<typename HS::WordType, N/sizeof(typename HC::WordType)> pubVarsXor; | |
GenericProgressBar progress; | |
static bool asc2hash(const std::string hash, typename HC::DigType &chkHash) | |
{ | |
vector<uint8_t> v; | |
const size_t digbytes = sizeof(typename HC::DigType); | |
if (!asciiHexToVector(hash, v) || v.size() != digbytes) | |
return false; | |
stringstream ss; | |
for (const auto& c : v) | |
ss.put(c); | |
for (auto& i : chkHash) | |
bless(i, ss); | |
return true; | |
} | |
void _init_pubXor(const vector<uint8_t> &PreXor) { | |
stringstream ss; | |
std::size_t i = 0; | |
for (const auto &c : PreXor) { | |
ss.put(c); | |
i++; | |
} | |
while (i++ < N) | |
ss.put('\0'); | |
for (auto &w : pubXor) { | |
bless(w, ss); | |
} | |
} | |
Algo(const vector<uint8_t> &PreXor, const std::string secret) | |
: progress(cerr, 50) | |
{ | |
auto xi = PreXor.begin(); | |
uint8_t x; | |
for (const auto& c : secret) { | |
x = (xi == PreXor.end() ? 0 : *(xi++)); | |
preImageFwd.push_back(c); | |
preImageBck.push_back(c ^ x); | |
} | |
assert(preImageFwd.size() <= N); | |
while(preImageFwd.size() < N) { | |
x = (xi == PreXor.end() ? 0 : *(xi++)); | |
preImageFwd.push_back('\0'); | |
preImageBck.push_back(x); | |
} | |
assert(xi == PreXor.end()); | |
assert(preImageFwd.size() == N); | |
assert(preImageBck.size() == N); | |
_init_pubXor(PreXor); | |
pubHashFwd = digest(HC(), preImageFwd); | |
pubHashBck = digest(HC(), preImageBck); | |
} | |
Algo(vector<uint8_t> &PreXor, | |
typename HC::DigType &chkHashFwd, | |
typename HC::DigType &chkHashBck) | |
: progress(cerr, 50) | |
{ | |
_init_pubXor(PreXor); | |
pubHashFwd = chkHashFwd; | |
pubHashBck = chkHashBck; | |
} | |
void _input_step() { | |
bless(pubVarsXor, pubXor); | |
bless(pubVarsFwd, pubHashFwd); | |
bless(pubVarsBck, pubHashBck); | |
end_input<PAIRING>(); | |
} | |
void _calc_step() { | |
vector<typename HS::WordType> blessFwd, blessBck; | |
assert_true(pubVarsFwd == do_digest(HS(), N, preImageFwd, blessFwd)); | |
assert_true(pubVarsBck == do_digest(HS(), N, preImageBck, blessBck)); | |
assert(blessFwd.size() == blessBck.size()); | |
assert(blessFwd.size() == pubXor.size()); | |
for (std::size_t i = 0; i < blessFwd.size(); i++) { | |
assert_true(blessFwd[i] == (blessBck[i] ^ pubVarsXor[i])); | |
} | |
} | |
void do_keygen(void) { | |
// trusted key generation | |
_input_step(); | |
_calc_step(); | |
// generate proving/verification key pair | |
cerr << "generate key pair"; | |
cout << keypair<PAIRING>(progress); // expensive! | |
cerr << endl; | |
} | |
void do_input(void) { | |
_input_step(); | |
// publicly known input variables | |
cout << input<PAIRING>(); | |
} | |
void do_proof(Keypair<PAIRING> &keypair) { | |
// generate a proof | |
// check for marshalling errors | |
assert(!keypair.empty()); | |
_input_step(); | |
_calc_step(); | |
// generate proof | |
cerr << "generate proof"; | |
cout << proof(keypair, progress); | |
cerr << endl; | |
} | |
void do_verify(Keypair<PAIRING> &keypair, Proof<PAIRING> &proof) { | |
_input_step(); | |
Input<PAIRING> inp = input<PAIRING>(); | |
// check for marshalling errors | |
assert(!keypair.empty() && !proof.empty()); | |
// verify proof | |
cerr << "verify proof "; | |
const bool valid = verify(keypair, inp, proof, progress); | |
cerr << endl; | |
cout << "proof is " << (valid ? "verified" : "rejected") << endl; | |
} | |
}; | |
int main(int argc, char *argv[]) | |
{ | |
const std::size_t hashbytes = 32; | |
Getopt cmdLine(argc, argv, "mshbx", "", ""); | |
if (!cmdLine || cmdLine.empty()) printUsage(argv[0]); | |
const auto mode = cmdLine.getString('m'); | |
const auto prefwd = cmdLine.getString('s'); | |
const auto hashfwd = cmdLine.getString('h'); | |
const auto hashbck = cmdLine.getString('b'); | |
const auto prexor = cmdLine.getString('x'); | |
typedef Algo<cryptl::SHA256,snarkfront::SHA256<FR>, hashbytes> SHA256Algo; | |
cryptl::SHA256::DigType chkHashFwd, chkHashBck; | |
vector<uint8_t> PreXor; | |
SHA256Algo *algo; | |
cerr << "m: " << mode << "." << endl; | |
cerr << "f: " << hashfwd << "." << endl; | |
cerr << "b: " << hashbck << "." << endl; | |
cerr << "x: " << prexor << "." << endl; | |
if (mode != "keygen") { | |
if (prexor == "") { | |
cerr << "Must supply xor" << endl; | |
return 1; | |
} | |
if (prefwd == "" && (hashfwd == "" || hashbck == "")) { | |
cerr << "Must supply hashes or preimage" << endl; | |
return 1; | |
} | |
if (!asciiHexToVector(prexor, PreXor) || PreXor.size() == 0) { | |
cerr << "Invalid xor string: " << prexor << "." << endl; | |
return 1; | |
} | |
if (PreXor.size() > hashbytes) { | |
cerr << "Xor string is too long (max " << hashbytes << "bytes)" << endl; | |
return 1; | |
} | |
if (prefwd.size() > hashbytes) { | |
cerr << "preimage is too long (max " << hashbytes << " bytes)" << endl; | |
return 1; | |
} | |
if (hashfwd != "") { | |
if (!SHA256Algo::asc2hash(hashfwd, chkHashFwd)) { | |
cerr << "Invalid hash string: " << hashfwd << "." << endl; | |
return 1; | |
} | |
} | |
if (hashbck != "") { | |
if (!SHA256Algo::asc2hash(hashbck, chkHashBck)) { | |
cerr << "Invalid hash string: " << hashbck << "." << endl; | |
return 1; | |
} | |
} | |
} | |
// Barreto-Naehrig 128 bits | |
init_BN128(); | |
if (prefwd == "") { | |
algo = new SHA256Algo(PreXor, chkHashFwd, chkHashBck); | |
} else { | |
algo = new SHA256Algo(PreXor, prefwd); | |
cerr << "F: " << asciiHex(algo->preImageFwd) << endl; | |
cerr << "B: " << asciiHex(algo->preImageBck) << endl; | |
cerr << "#F: " << asciiHex(algo->pubHashFwd) << endl; | |
cerr << "#B: " << asciiHex(algo->pubHashBck) << endl; | |
if (hashfwd != "") { | |
if (chkHashFwd != algo->pubHashFwd) { | |
cerr << "Supplied forward hash does not match preimage hash" << endl; | |
return 1; | |
} | |
} | |
if (hashbck != "") { | |
if (chkHashBck != algo->pubHashBck) { | |
cerr << "Supplied backward hash does not match calculated preimage hash" << endl; | |
return 1; | |
} | |
} | |
} | |
if ("keygen" == mode) { | |
algo->do_keygen(); | |
} else if ("input" == mode) { | |
algo->do_input(); | |
} else if ("proof" == mode) { | |
Keypair<PAIRING> keypair; // proving/verification key pair | |
cin >> keypair; | |
algo->do_proof(keypair); | |
} else if ("verify" == mode) { | |
// verify a proof | |
Keypair<PAIRING> keypair; // proving/verification key pair | |
Proof<PAIRING> proof; // zero knowledge proof | |
cin >> keypair >> proof; | |
algo->do_verify(keypair, proof); | |
} else { | |
// no mode specified | |
printUsage(argv[0]); | |
} | |
return EXIT_SUCCESS; | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment