Skip to content

Add a sha3_256 module with SHA3-256 - #4919

Merged
apoelstra merged 2 commits into
rust-bitcoin:masterfrom
rustaceanrob:8-28-sha3-hash
Sep 7, 2025
Merged

apoelstra merged 2 commits into
rust-bitcoin:masterfrom
rustaceanrob:8-28-sha3-hash

Conversation

@rustaceanrob

@rustaceanrob rustaceanrob commented Aug 29, 2025 •

Copy link
Copy Markdown
Member

My first commit message motivates this change. The brief explanation is Tor nodes are advertised by ED25519 public key, but are often discoverable as .onion addresses. To go from public key to onion address, one must take SHA3(".onion checksum" | PUBKEY | VERSION) for a checksum. I will leave the rest for the commit message, as well as all reference material.

@github-actions github-actions Bot added the C-hashes PRs modifying the hashes crate label Aug 29, 2025
@rustaceanrob
rustaceanrob force-pushed the 8-28-sha3-hash branch 4 times, most recently from 66beddc to a175470 Compare August 29, 2025 12:01
@rustaceanrob rustaceanrob changed the title Add a sha3 module with SHA3-256 Add a sha3_256 module with SHA3-256 Aug 29, 2025
@rustaceanrob
rustaceanrob marked this pull request as ready for review August 29, 2025 12:33

@jrakibi jrakibi left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I validated the implementation using additional test vectors from NIST CAVP SHA3-256 suite

They can be found in my branch here. All inputs pass successfully.

I’ve left some small suggestions in the review comments

Comment thread hashes/src/sha3_256/mod.rs
Comment thread hashes/src/sha3_256/mod.rs
Comment thread hashes/src/sha3_256/mod.rs Outdated
@rustaceanrob

Copy link
Copy Markdown
Member Author

They can be found in my branch here

My friend how were you able to add 156 cases? I am unsure to what extend other reviewers would like this hash tested, but great work and thanks for further validation.

@jrakibi

jrakibi commented Aug 30, 2025

Copy link
Copy Markdown
Contributor

how were you able to add 156 cases?

All the cases come from https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Algorithm-Validation-Program/documents/sha3/sha-3bytetestvectors.zip

I didn’t include all of the very long message cases from SHA3_256LongMsg.rsp file, since I think the ones we included already cover the critical edge cases

@apoelstra

Copy link
Copy Markdown
Member

concept ACK adding this hash.

@jrakibi great find!! Yeah, for sure, we should throw all the test cases we can find at this. (But honestly, personally, I think that if you just test the empty string and any string longer than the input to the compression function, and your code looks roughly like a hash function should, it's overwhelmingly likely that your hash is correct.)

@rustaceanrob

rustaceanrob commented Aug 31, 2025 •

Copy link
Copy Markdown
Member Author

All the cases come from

Right, but how did you go from the rsp file to strings in the test file?

I can either 1. add the rsp file to contrib and parse it in the unit tests 2. add them directly to the file as @jrakibi did. Is there a preference for either approach?

@jrakibi

jrakibi commented Aug 31, 2025

Copy link
Copy Markdown
Contributor

but how did you go from the rsp file to strings in the test file?

yaa this is the python script to go from .rsp file to strings, in case you’re following the second approach.

#!/usr/bin/env python3
import sys
import re

def parse_rsp_file(filename):
    with open(filename, 'r') as f:
        content = f.read()
    
    pattern = r'Len\s*=\s*(\d+)\s*\nMsg\s*=\s*([a-fA-F0-9]*)\s*\nMD\s*=\s*([a-fA-F0-9]+)'
    matches = re.findall(pattern, content)
    
    test_cases = []
    for length, msg, md in matches:
        if length == "0" and msg == "00":
            msg = ""
        test_cases.append({'input': msg, 'output': md.lower()})
    
    return test_cases

def main():
    test_cases = parse_rsp_file(sys.argv[1])
    
    print("const TESTS: [TestCase; {}] = [".format(len(test_cases)))
    for case in test_cases:
        print(f"        TestCase {{")
        print(f"            input: \"{case['input']}\",")
        print(f"            output: \"{case['output']}\"")
        print(f"        }},")
    print("];")

if __name__ == "__main__":
    main()

Then you can run it for the two files SHA3_256ShortMsg.rsp and SHA3_256LongMsg.rsp:

$ python3 convert_nist_tests.py "SHA3_256ShortMsg.rsp"

@rustaceanrob

rustaceanrob commented Aug 31, 2025 •

Copy link
Copy Markdown
Member Author

6f878c7 includes the NIST test vectors, many short and some long, within the file. Thanks a bunch for that script @jrakibi

I think that if you just test the empty string and any string longer than the input to the compression function ... it's overwhelmingly likely that your hash is correct.

These cases are now covered by all the NIST test vectors.

Comment thread hashes/src/sha3_256/mod.rs Outdated
Comment thread hashes/src/sha3_256/mod.rs Outdated
@tcharding

tcharding commented Sep 2, 2025 •

Copy link
Copy Markdown
Member

There is no Hash impl, is that on purpose? By that I mean sha3_256::Hash::engine() (and other functions) do not exist.

@tcharding

Copy link
Copy Markdown
Member

You could add the new hash to the tests/api.rs file to see what is different.

@rustaceanrob

rustaceanrob commented Sep 2, 2025 •

Copy link
Copy Markdown
Member Author

There is no Hash impl

When I invoke general_hash_type I get a clippy error I am lost on. I will push that up so you can have a look.

I had a dull moment. I added the Hash implementation along with the api tests in the first commit.

@github-actions github-actions Bot added the test label Sep 2, 2025
@rustaceanrob
rustaceanrob force-pushed the 8-28-sha3-hash branch 3 times, most recently from 87bce8f to e4de9b0 Compare September 2, 2025 10:17
Comment thread hashes/src/sha3_256/mod.rs Outdated
Comment thread hashes/src/sha3_256/mod.rs Outdated
Comment thread hashes/src/sha3_256/mod.rs Outdated
Comment thread hashes/src/sha3_256/mod.rs Outdated
Comment thread hashes/src/sha3_256/mod.rs Outdated
Comment thread hashes/src/sha3_256/mod.rs Outdated

@tcharding tcharding left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All my comments are nits mate except the _ function names. It looks good to me. Code review only, no crypto review but since its so well tested I don't think it needs deep crypto knowledge. I don't know anything about SIMD either.

EDIT: oh drats, this shows up below all the nits because I used "Add single comment" instead of "Start review" - hope I didn't trigger you too hard.

There is a substantial amount of bitcoin listening nodes only reachable
by Tor. This is backed by some relatively old research, but I would
imagine this remains the case today. When it comes to peer-to-peer
gossip, Tor nodes are reachable by a 32-byte ed25519 public key. In
practice, most applications will run a Tor daemon, and other
applications will communicate with Tor via a Socks5 proxy. With Socks5,
you may connect to an IPv4, IPv6, or domain.

The problem is to go from Tor public key to onion address (domain), you
must take a Sha3-256 hash of a public key and some concatinated data. In
effect this makes Tor nodes unreachable for those that do not want to
use `RustCrypto` or god forbid the Arti project.

Here I propose we add this hash to `bitcoin_hashes`. While it is not
explicitly a hash used in bitcoin, many bitcoin nodes use Tor, and more
importantly, accept inbound connections using Tor. Others are of course
free to use this hash however their heart desires.

As far as implementation, from a high level, the hash is a number of
permutation operations on a state array of 1600 bits. These bits can be
visualized/represented as a 5x5 matrix of 64-bit "lanes." I will leave
the rest to the code comment in the file. The Keccak team designed this
function to be robust theoretically but easy to implement. Each of these
lanes may be mutated directly, and I also try to abuse `for_each` to
give SIMD hints. To review the implementation, you may follow the
psuedo-code provided by the Keccak team, linked below.

ref
Psuedo-code: https://keccak.team/keccak_specs_summary.html
Full Keccak reference: https://keccak.team/files/Keccak-reference-3.0.pdf
Tor spec: https://spec.torproject.org/rend-spec/encoding-onion-addresses.html
Tor post: https://archive.torproject.org/websites/lists.torproject.org/pipermail/tor-dev/2017-January/011816.html
P2P data: https://github.com/virtu/p2p-metrics
All tests are sourced from NIST via the Cryptographic Algorithm
Validation Program. They include namely:
1. the null data case
2. Input cases just above, equal, and below the rate block in bytes
3. Additional randomly sized vectors

ref: https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program/secure-hashing
@rustaceanrob

Copy link
Copy Markdown
Member Author

5fa8a5d implements most of the cleanup. 5524934 most current head

@rustaceanrob

Copy link
Copy Markdown
Member Author

I don't know anything about SIMD either

Nor do I. From what I understand making function calls inline and using for_each should do the heavy lifting for the compiler. @nyonson do you have any further insight on SIMD related stuff?

Otherwise I think this is sufficiently tested and I don't have anything else I'd like to iterate on.

@nyonson

nyonson commented Sep 3, 2025

Copy link
Copy Markdown
Contributor

@nyonson do you have any further insight on SIMD related stuff?

Maybe overkill given the bitcoin ecosystem use case, but we could take a similar strategy as the chacha20 crate and target the experimental portable SIMD interface. That allowed the modern rust compiler to squeeze the most SIMD juice without writing machine code. And has the promise 🤞 of being dropped for the std lib version in the future.

Basic idea is to copy what the interface would be for U64x5. There isn't a nice type alias like there is for the U64x4 variant used in chacha, but I think it is pretty straight forward, type U64x5 = Simd<u64, 5>;. It will probably make this impl a little tougher to read, but I'd bet it would speed up performance.

Might want to add a simple benches.rs here to make it easy to see if SIMD tweaks have an effect.

@tcharding

Copy link
Copy Markdown
Member

Can we do all that as a follow up if you guys decide to do it? Makes it easier to get this in.

@tcharding tcharding left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 5524934

@rustaceanrob

Copy link
Copy Markdown
Member Author

Can we do all that as a follow up

Yes, preferred

Maybe overkill given the bitcoin ecosystem use case

My estimate is this would be called at a maximum around every ~100 ms if someone really needs to connect to Tor nodes. I do think we should follow up with U64x5 to see if there is an order-of-magnitude improvement, but for now I will table it. The current state is legible compared to the example code from the Keccak team.

@rustaceanrob

Copy link
Copy Markdown
Member Author

I couldn't help but run a bench. I would still rather add this in a separate PR, but I thought I would share the results. They are uneventful, which is definitely de-motivating to optimize further, but there seems to be room for improvement. For the immediately pressing use-case the current implementation appears fine.

test hash160::benches::hash160_10               ... bench:           5.59 ns/iter (+/- 0.09) = 2000 MB/s
test hash160::benches::hash160_1k               ... bench:         548.31 ns/iter (+/- 5.75) = 1868 MB/s
test hash160::benches::hash160_64k              ... bench:      35,069.05 ns/iter (+/- 1,274.15) = 1868 MB/s
test hmac::benches::hmac_sha256_10              ... bench:           5.65 ns/iter (+/- 0.06) = 2000 MB/s
test hmac::benches::hmac_sha256_1k              ... bench:         551.80 ns/iter (+/- 17.77) = 1858 MB/s
test hmac::benches::hmac_sha256_64k             ... bench:      35,027.11 ns/iter (+/- 931.46) = 1871 MB/s
test ripemd160::benches::ripemd160_10           ... bench:          23.82 ns/iter (+/- 0.32) = 434 MB/s
test ripemd160::benches::ripemd160_1k           ... bench:       2,266.72 ns/iter (+/- 23.92) = 451 MB/s
test ripemd160::benches::ripemd160_64k          ... bench:     144,780.10 ns/iter (+/- 3,057.04) = 452 MB/s
test sha1::benches::sha1_10                     ... bench:          21.90 ns/iter (+/- 0.44) = 476 MB/s
test sha1::benches::sha1_1k                     ... bench:       2,002.04 ns/iter (+/- 25.87) = 511 MB/s
test sha1::benches::sha1_64k                    ... bench:     128,062.21 ns/iter (+/- 1,800.28) = 511 MB/s
test sha256::benches::sha256_10                 ... bench:           5.60 ns/iter (+/- 0.11) = 2000 MB/s
test sha256::benches::sha256_1k                 ... bench:         548.15 ns/iter (+/- 18.21) = 1868 MB/s
test sha256::benches::sha256_64k                ... bench:      35,183.27 ns/iter (+/- 553.38) = 1862 MB/s
test sha256d::benches::sha256d_10               ... bench:           5.60 ns/iter (+/- 0.05) = 2000 MB/s
test sha256d::benches::sha256d_1k               ... bench:         549.59 ns/iter (+/- 9.94) = 1865 MB/s
test sha256d::benches::sha256d_64k              ... bench:      35,317.62 ns/iter (+/- 798.49) = 1855 MB/s
test sha384::benches::sha384_10                 ... bench:          14.04 ns/iter (+/- 0.17) = 714 MB/s
test sha384::benches::sha384_1k                 ... bench:       1,233.33 ns/iter (+/- 16.48) = 830 MB/s
test sha384::benches::sha384_64k                ... bench:      78,911.82 ns/iter (+/- 1,278.38) = 830 MB/s
test sha3_256::benches::sha3_256_10             ... bench:         245.14 ns/iter (+/- 3.58) = 40 MB/s
test sha3_256::benches::sha3_256_1k             ... bench:       1,961.34 ns/iter (+/- 14.22) = 522 MB/s
test sha3_256::benches::sha3_256_64k            ... bench:     117,535.57 ns/iter (+/- 855.62) = 557 MB/s
test sha512::benches::sha512_10                 ... bench:          14.09 ns/iter (+/- 0.28) = 714 MB/s
test sha512::benches::sha512_1k                 ... bench:       1,229.46 ns/iter (+/- 20.87) = 833 MB/s
test sha512::benches::sha512_64k                ... bench:      78,721.40 ns/iter (+/- 1,274.06) = 832 MB/s
test sha512_256::benches::sha512_256_10         ... bench:          14.00 ns/iter (+/- 0.31) = 714 MB/s
test sha512_256::benches::sha512_256_1k         ... bench:       1,237.35 ns/iter (+/- 19.17) = 827 MB/s
test sha512_256::benches::sha512_256_64k        ... bench:      78,909.24 ns/iter (+/- 1,253.37) = 830 MB/s
test siphash24::benches::siphash24_1ki          ... bench:         290.47 ns/iter (+/- 2.78) = 3531 MB/s
test siphash24::benches::siphash24_1ki_hash     ... bench:         291.69 ns/iter (+/- 6.72) = 3518 MB/s
test siphash24::benches::siphash24_1ki_hash_u64 ... bench:         285.80 ns/iter (+/- 5.67) = 3592 MB/s
test siphash24::benches::siphash24_64ki         ... bench:      18,487.62 ns/iter (+/- 359.44) = 3544 MB/s
diff --git a/hashes/src/sha3_256/benches.rs b/hashes/src/sha3_256/benches.rs
new file mode 100644
index 00000000..f27f0d9e
--- /dev/null
+++ b/hashes/src/sha3_256/benches.rs
@@ -0,0 +1,33 @@
+use test::Bencher;
+
+use crate::{sha256, Hash, HashEngine};
+
+#[bench]
+pub fn sha3_256_10(bh: &mut Bencher) {
+    let mut engine = crate::sha3_256::Hash::engine();
+    let bytes = [1u8; 10];
+    bh.iter(|| {
+        engine.input(&bytes);
+    });
+    bh.bytes = bytes.len() as u64;
+}
+
+#[bench]
+pub fn sha3_256_1k(bh: &mut Bencher) {
+    let mut engine = crate::sha3_256::Hash::engine();
+    let bytes = [1u8; 1024];
+    bh.iter(|| {
+        engine.input(&bytes);
+    });
+    bh.bytes = bytes.len() as u64;
+}
+
+#[bench]
+pub fn sha3_256_64k(bh: &mut Bencher) {
+    let mut engine = crate::sha3_256::Hash::engine();
+    let bytes = [1u8; 65536];
+    bh.iter(|| {
+        engine.input(&bytes);
+    });
+    bh.bytes = bytes.len() as u64;
+}
diff --git a/hashes/src/sha3_256/mod.rs b/hashes/src/sha3_256/mod.rs
index eccf027a..ee150beb 100644
--- a/hashes/src/sha3_256/mod.rs
+++ b/hashes/src/sha3_256/mod.rs
@@ -17,6 +17,8 @@
 //
 // To read this file, follow the example code: https://keccak.team/keccak_specs_summary.html
 // For a detailed specification: https://keccak.team/files/Keccak-reference-3.0.pdf
+#[cfg(bench)]
+mod benches;
 use core::fmt;

 crate::internal_macros::general_hash_type! {

@apoelstra apoelstra left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 5524934; successfully ran local tests

@apoelstra
apoelstra merged commit f697cc7 into rust-bitcoin:master Sep 7, 2025
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C-hashes PRs modifying the hashes crate test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants