From f9980aaa7e4667cdb0556ef85ba2991897109447 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Mon, 15 May 2023 15:05:25 -0400 Subject: [PATCH] add Bao functionality --- bao.go | 146 +++++++++++++++++++++++++++++++++++++++ bao_test.go | 107 ++++++++++++++++++++++++++++ blake3_test.go | 6 +- compress_amd64.go | 8 +++ compress_noasm.go | 17 +++++ go.mod | 2 +- testdata/bao-golden.bao | Bin 0 -> 20845 bytes testdata/bao-golden.obao | Bin 0 -> 1224 bytes 8 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 bao.go create mode 100644 bao_test.go create mode 100644 testdata/bao-golden.bao create mode 100644 testdata/bao-golden.obao diff --git a/bao.go b/bao.go new file mode 100644 index 0000000..01eb12b --- /dev/null +++ b/bao.go @@ -0,0 +1,146 @@ +package blake3 + +import ( + "bytes" + "encoding/binary" + "io" + "math/bits" +) + +// BaoEncode computes the intermediate BLAKE3 tree hashes of data and writes +// them to dst. If outboard is false, the contents of data are also written to +// dst, interleaved with the tree hashes. It also returns the tree root, i.e. +// the 256-bit BLAKE3 hash. +func BaoEncode(dst io.WriterAt, data io.Reader, dataLen int64, outboard bool) ([32]byte, error) { + var counter uint64 + var chunkBuf [chunkSize]byte + var err error + read := func(p []byte) []byte { + if err == nil { + _, err = io.ReadFull(data, p) + } + return p + } + write := func(p []byte, off uint64) { + if err == nil { + _, err = dst.WriteAt(p, int64(off)) + } + } + + // NOTE: unlike the reference implementation, we write directly in + // pre-order, rather than writing in post-order and then flipping. This cuts + // the I/O required in half, but also makes hashing multiple chunks in SIMD + // a lot trickier. I'll save that optimization for a rainy day. + var rec func(bufLen uint64, flags uint32, off uint64) (uint64, [8]uint32) + rec = func(bufLen uint64, flags uint32, off uint64) (uint64, [8]uint32) { + if err != nil { + return 0, [8]uint32{} + } else if bufLen <= chunkSize { + cv := chainingValue(compressChunk(read(chunkBuf[:bufLen]), &iv, counter, flags)) + counter++ + if !outboard { + write(chunkBuf[:bufLen], off) + } + return 0, cv + } + mid := uint64(1) << (bits.Len64(bufLen-1) - 1) + lchildren, l := rec(mid, 0, off+64) + llen := lchildren * 32 + if !outboard { + llen += (mid / chunkSize) * chunkSize + } + rchildren, r := rec(bufLen-mid, 0, off+64+llen) + write(cvToBytes(&l)[:], off) + write(cvToBytes(&r)[:], off+32) + return 2 + lchildren + rchildren, chainingValue(parentNode(l, r, iv, flags)) + } + + binary.LittleEndian.PutUint64(chunkBuf[:8], uint64(dataLen)) + write(chunkBuf[:8], 0) + _, root := rec(uint64(dataLen), flagRoot, 8) + return *cvToBytes(&root), err +} + +// BaoDecode reads content and tree data from the provided reader(s), and +// streams the verified content to dst. It returns false if verification fails. +// If the content and tree data are interleaved, outboard should be nil. +func BaoDecode(dst io.Writer, data, outboard io.Reader, root [32]byte) (bool, error) { + if outboard == nil { + outboard = data + } + var counter uint64 + var buf [chunkSize]byte + var err error + read := func(r io.Reader, p []byte) []byte { + if err == nil { + _, err = io.ReadFull(r, p) + } + return p + } + readParent := func() (l, r [8]uint32) { + read(outboard, buf[:64]) + return bytesToCV(buf[:32]), bytesToCV(buf[32:]) + } + + var rec func(cv [8]uint32, bufLen uint64, flags uint32) bool + rec = func(cv [8]uint32, bufLen uint64, flags uint32) bool { + if err != nil { + return false + } else if bufLen <= chunkSize { + n := compressChunk(read(data, buf[:bufLen]), &iv, counter, flags) + counter++ + return cv == chainingValue(n) + } + l, r := readParent() + n := parentNode(l, r, iv, flags) + mid := uint64(1) << (bits.Len64(bufLen-1) - 1) + return chainingValue(n) == cv && rec(l, mid, 0) && rec(r, bufLen-mid, 0) + } + + read(outboard, buf[:8]) + dataLen := binary.LittleEndian.Uint64(buf[:8]) + ok := rec(bytesToCV(root[:]), dataLen, flagRoot) + return ok, err +} + +type bufferAt struct { + buf []byte +} + +func (b *bufferAt) WriteAt(p []byte, off int64) (int, error) { + if copy(b.buf[off:], p) != len(p) { + panic("bad buffer size") + } + return len(p), nil +} + +func baoOutboardSize(dataLen int) int { + if dataLen == 0 { + return 8 + } + chunks := (dataLen + chunkSize - 1) / chunkSize + cvs := 2*chunks - 2 // no I will not elaborate + return 8 + cvs*32 +} + +// BaoEncodeBuf returns the Bao encoding and root (i.e. BLAKE3 hash) for data. +func BaoEncodeBuf(data []byte, outboard bool) ([]byte, [32]byte) { + bufSize := baoOutboardSize(len(data)) + if !outboard { + bufSize += len(data) + } + buf := bufferAt{buf: make([]byte, bufSize)} + root, _ := BaoEncode(&buf, bytes.NewReader(data), int64(len(data)), outboard) + return buf.buf, root +} + +// BaoVerifyBuf verifies the Bao encoding and root (i.e. BLAKE3 hash) for data. +// If the content and tree data are interleaved, outboard should be nil. +func BaoVerifyBuf(data, outboard []byte, root [32]byte) bool { + var or io.Reader = bytes.NewReader(outboard) + if outboard == nil { + or = nil + } + ok, _ := BaoDecode(io.Discard, bytes.NewReader(data), or, root) + return ok +} diff --git a/bao_test.go b/bao_test.go new file mode 100644 index 0000000..bbcac9f --- /dev/null +++ b/bao_test.go @@ -0,0 +1,107 @@ +package blake3_test + +import ( + "bytes" + "os" + "testing" + + "lukechampine.com/blake3" +) + +func TestBaoGolden(t *testing.T) { + data, err := os.ReadFile("testdata/vectors.json") + if err != nil { + t.Fatal(err) + } + goldenInterleaved, err := os.ReadFile("testdata/bao-golden.bao") + if err != nil { + t.Fatal(err) + } + goldenOutboard, err := os.ReadFile("testdata/bao-golden.obao") + if err != nil { + t.Fatal(err) + } + + interleaved, root := blake3.BaoEncodeBuf(data, false) + if toHex(root[:]) != "6654fbd1836b531b25e2782c9cc9b792c80abb36b024f59db5d5f6bd3187ddfe" { + t.Errorf("bad root: %x", root) + } else if !bytes.Equal(interleaved, goldenInterleaved) { + t.Error("bad interleaved encoding") + } + + outboard, root := blake3.BaoEncodeBuf(data, true) + if toHex(root[:]) != "6654fbd1836b531b25e2782c9cc9b792c80abb36b024f59db5d5f6bd3187ddfe" { + t.Errorf("bad root: %x", root) + } else if !bytes.Equal(outboard, goldenOutboard) { + t.Error("bad outboard encoding") + } + + // test empty input + interleaved, root = blake3.BaoEncodeBuf(nil, false) + if toHex(root[:]) != "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262" { + t.Errorf("bad root: %x", root) + } else if toHex(interleaved[:]) != "0000000000000000" { + t.Errorf("bad interleaved encoding: %x", interleaved) + } else if !blake3.BaoVerifyBuf(interleaved, nil, root) { + t.Error("verify failed") + } + outboard, root = blake3.BaoEncodeBuf(nil, false) + if toHex(root[:]) != "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262" { + t.Errorf("bad root: %x", root) + } else if toHex(outboard[:]) != "0000000000000000" { + t.Errorf("bad outboard encoding: %x", outboard) + } else if !blake3.BaoVerifyBuf(nil, outboard, root) { + t.Error("verify failed") + } +} + +func TestBaoInterleaved(t *testing.T) { + data, _ := os.ReadFile("testdata/vectors.json") + interleaved, root := blake3.BaoEncodeBuf(data, false) + if !blake3.BaoVerifyBuf(interleaved, nil, root) { + t.Fatal("verify failed") + } + badRoot := root + badRoot[0] ^= 1 + if blake3.BaoVerifyBuf(interleaved, nil, badRoot) { + t.Fatal("verify succeeded with bad root") + } + badPrefix := append([]byte(nil), interleaved...) + badPrefix[0] ^= 1 + if blake3.BaoVerifyBuf(badPrefix, nil, root) { + t.Fatal("verify succeeded with bad length prefix") + } + badCVs := append([]byte(nil), interleaved...) + badCVs[8] ^= 1 + if blake3.BaoVerifyBuf(badCVs, nil, root) { + t.Fatal("verify succeeded with bad cv data") + } + badData := append([]byte(nil), interleaved...) + badData[len(badData)-1] ^= 1 + if blake3.BaoVerifyBuf(badData, nil, root) { + t.Fatal("verify succeeded with bad content") + } +} + +func TestBaoOutboard(t *testing.T) { + data, _ := os.ReadFile("testdata/vectors.json") + outboard, root := blake3.BaoEncodeBuf(data, true) + if !blake3.BaoVerifyBuf(data, outboard, root) { + t.Fatal("verify failed") + } + badRoot := root + badRoot[0] ^= 1 + if blake3.BaoVerifyBuf(data, outboard, badRoot) { + t.Fatal("verify succeeded with bad root") + } + badPrefix := append([]byte(nil), outboard...) + badPrefix[0] ^= 1 + if blake3.BaoVerifyBuf(data, badPrefix, root) { + t.Fatal("verify succeeded with bad length prefix") + } + badCVs := append([]byte(nil), outboard...) + badCVs[8] ^= 1 + if blake3.BaoVerifyBuf(data, badCVs, root) { + t.Fatal("verify succeeded with bad cv data") + } +} diff --git a/blake3_test.go b/blake3_test.go index e08de27..8a2f566 100644 --- a/blake3_test.go +++ b/blake3_test.go @@ -5,7 +5,7 @@ import ( "encoding/hex" "encoding/json" "io" - "io/ioutil" + "os" "testing" "lukechampine.com/blake3" @@ -22,7 +22,7 @@ var testVectors = func() (vecs struct { DeriveKey string `json:"derive_key"` } }) { - data, err := ioutil.ReadFile("testdata/vectors.json") + data, err := os.ReadFile("testdata/vectors.json") if err != nil { panic(err) } @@ -197,7 +197,7 @@ func BenchmarkWrite(b *testing.B) { func BenchmarkXOF(b *testing.B) { b.ReportAllocs() b.SetBytes(1024) - io.CopyN(ioutil.Discard, blake3.New(0, nil).XOF(), int64(b.N*1024)) + io.CopyN(io.Discard, blake3.New(0, nil).XOF(), int64(b.N*1024)) } func BenchmarkSum256(b *testing.B) { diff --git a/compress_amd64.go b/compress_amd64.go index fa2eb11..b647b6d 100644 --- a/compress_amd64.go +++ b/compress_amd64.go @@ -134,3 +134,11 @@ func mergeSubtrees(cvs *[maxSIMD][8]uint32, numCVs uint64, key *[8]uint32, flags func wordsToBytes(words [16]uint32, block *[64]byte) { *block = *(*[64]byte)(unsafe.Pointer(&words)) } + +func bytesToCV(b []byte) [8]uint32 { + return *(*[8]uint32)(unsafe.Pointer(&b[0])) +} + +func cvToBytes(cv *[8]uint32) *[32]byte { + return (*[32]byte)(unsafe.Pointer(cv)) +} diff --git a/compress_noasm.go b/compress_noasm.go index 0d30ba2..c38819d 100644 --- a/compress_noasm.go +++ b/compress_noasm.go @@ -1,3 +1,4 @@ +//go:build !amd64 // +build !amd64 package blake3 @@ -74,3 +75,19 @@ func wordsToBytes(words [16]uint32, block *[64]byte) { binary.LittleEndian.PutUint32(block[4*i:], w) } } + +func bytesToCV(b []byte) [8]uint32 { + var cv [8]uint32 + for i := range cv { + cv[i] = binary.LittleEndian.Uint32(b[4*i:]) + } + return cv +} + +func cvToBytes(cv *[8]uint32) *[32]byte { + var b [32]byte + for i, w := range cv { + binary.LittleEndian.PutUint32(b[4*i:], w) + } + return &b +} diff --git a/go.mod b/go.mod index 8f0680f..c94c10c 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module lukechampine.com/blake3 -go 1.13 +go 1.17 require github.com/klauspost/cpuid/v2 v2.0.9 diff --git a/testdata/bao-golden.bao b/testdata/bao-golden.bao new file mode 100644 index 0000000000000000000000000000000000000000..d6e8149d1a565ea6a4859533f51d2cdea8b1495b GIT binary patch literal 20845 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 literal 0 HcmV?d00001