diff --git a/chaincfg/chainhash/hash.go b/chaincfg/chainhash/hash.go index ff35a126..cabb8352 100644 --- a/chaincfg/chainhash/hash.go +++ b/chaincfg/chainhash/hash.go @@ -7,10 +7,12 @@ package chainhash import ( "crypto/sha256" + "crypto/sha512" "encoding/binary" "encoding/hex" "encoding/json" "fmt" + "io" "math" ) @@ -58,6 +60,14 @@ var ( string(TagTapBranch): sha256.Sum256(TagTapBranch), string(TagTapTweak): sha256.Sum256(TagTapTweak), } + + // TagUtreexoV1 is the tag used by utreexo v1 serialized hashes to + // generate the leaf hashes to be committed into the accumulator. + TagUtreexoV1 = []byte("UtreexoV1") + + precomputedUtreexoTags = map[string][64]byte{ + string(TagUtreexoV1): sha512.Sum512(TagUtreexoV1), + } ) // ErrHashStrSize describes an error that indicates the caller specified a hash @@ -173,6 +183,27 @@ func TaggedHash(tag []byte, msgs ...[]byte) *Hash { return hash } +// TaggedHash512_256 implements a tagged hash scheme for utreexo leaves. We use +// sha-512_256 to bind a message hash to a specific context using a tag: +// sha512_256(sha512(tag) || sha512(tag) || leafdata). +func TaggedHash512_256(tag []byte, serialize func(io.Writer)) *Hash { + // Check to see if we've already pre-computed the hash of the tag. If + // so then this'll save us an extra sha512 hash. + shaTag, ok := precomputedUtreexoTags[string(tag)] + if !ok { + shaTag = sha512.Sum512(tag) + } + + // h = sha512_256(sha512(tag) || sha512(tag) || leafdata) + h := sha512.New512_256() + h.Write(shaTag[:]) + h.Write(shaTag[:]) + serialize(h) + + taggedHash := h.Sum(nil) + return (*Hash)(taggedHash) +} + // NewHashFromStr creates a Hash from a hash string. The string should be // the hexadecimal string of a byte-reversed hash, but any missing characters // result in zero padding at the end of the Hash. diff --git a/chaincfg/chainhash/hash_test.go b/chaincfg/chainhash/hash_test.go index df0aa148..6d876dd5 100644 --- a/chaincfg/chainhash/hash_test.go +++ b/chaincfg/chainhash/hash_test.go @@ -6,8 +6,10 @@ package chainhash import ( "bytes" + "crypto/sha512" "encoding/hex" "encoding/json" + "io" "reflect" "testing" ) @@ -243,3 +245,32 @@ func TestPackedHashes(t *testing.T) { } } } + +func TestTaggedHash512_256(t *testing.T) { + tests := []struct { + tagStr string + msg string + }{ + {tagStr: "UtreexoV1", msg: "hi"}, + {tagStr: "UtreexoV1", msg: "12354561654"}, + {tagStr: "hi", msg: "12354561654"}, + } + + for _, test := range tests { + var serialized bytes.Buffer + tag, found := precomputedUtreexoTags[test.tagStr] + if !found { + tag = sha512.Sum512([]byte(test.tagStr)) + } + serialized.Write(tag[:]) + serialized.Write(tag[:]) + serialized.Write([]byte(test.msg)) + + expect := sha512.Sum512_256(serialized.Bytes()) + + got := TaggedHash512_256([]byte(test.tagStr), func(w io.Writer) { w.Write([]byte(test.msg)) }) + if !bytes.Equal(got[:], expect[:]) { + t.Fatalf("expected %s, got %s", hex.EncodeToString(expect[:]), hex.EncodeToString(got[:])) + } + } +} diff --git a/wire/leaf.go b/wire/leaf.go index e92292ef..6bdb8c5d 100644 --- a/wire/leaf.go +++ b/wire/leaf.go @@ -6,7 +6,6 @@ package wire import ( "bytes" - "crypto/sha512" "encoding/hex" "encoding/json" "fmt" @@ -115,14 +114,11 @@ func (l *LeafData) UnmarshalJSON(data []byte) error { // LeafHash concats and hashes all the data in LeafData. func (l *LeafData) LeafHash() [32]byte { - digest := sha512.New512_256() - l.Serialize(digest) - - // TODO go 1.17 support slice to array conversion so we - // can avoid this extra copy. - hash := [32]byte{} - copy(hash[:], digest.Sum(nil)) - return hash + return *chainhash.TaggedHash512_256(chainhash.TagUtreexoV1, + func(w io.Writer) { + l.Serialize(w) + }, + ) } // ToString turns a LeafData into a string for logging. diff --git a/wire/leaf_test.go b/wire/leaf_test.go index c093dc45..250b9a9e 100644 --- a/wire/leaf_test.go +++ b/wire/leaf_test.go @@ -6,6 +6,7 @@ package wire import ( "bytes" + "crypto/sha512" "encoding/hex" "encoding/json" "fmt" @@ -630,3 +631,67 @@ func TestIsCompact(t *testing.T) { } } } + +func TestLeafHash(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + ld LeafData + }{ + { + name: "Testnet3 tx 061bb0bf... from block 1600000", + ld: LeafData{ + BlockHash: *newHashFromStr("00000000000172ff8a4e14441512072bacaf8d38b995a3fcd2f8435efc61717d"), + OutPoint: OutPoint{ + Hash: *newHashFromStr("061bb0bf3a1b9df13773da06bf92920394887a9c2b8b8772ac06be4e077df5eb"), + Index: 10, + }, + Amount: 200000, + PkScript: hexToBytes("a914e8d74935cfa223f9750a32b18d609cba17a5c3fe87"), + Height: 1599255, + IsCoinBase: false, + }, + }, + { + name: "Mainnet coinbase tx fa201b65... from block 573123", + ld: LeafData{ + BlockHash: *newHashFromStr("000000000000000000278eb9386b4e70b850a4ec21907af3a27f50330b7325aa"), + OutPoint: OutPoint{ + Hash: *newHashFromStr("fa201b650eef761f5701afbb610e4a211b86985da4745aec3ac0f4b7a8e2c8d2"), + Index: 0, + }, + Amount: 1315080370, + PkScript: hexToBytes("76a9142cc2b87a28c8a097f48fcc1d468ced6e7d39958d88ac"), + Height: 573123, + IsCoinBase: true, + }, + }, + } + + // Just a hash func for sanity checking here. + hashFunc := func(ld LeafData) [32]byte { + shaTag := sha512.Sum512(chainhash.TagUtreexoV1) + preimage := make([]byte, 0, (64*2)+ld.SerializeSize()) + preimage = append(preimage, shaTag[:]...) + preimage = append(preimage, shaTag[:]...) + var buf bytes.Buffer + err := ld.Serialize(&buf) + if err != nil { + t.Fatal(err) + } + preimage = append(preimage, buf.Bytes()...) + + return sha512.Sum512_256(preimage) + } + + for _, test := range tests { + got := test.ld.LeafHash() + expect := hashFunc(test.ld) + if got != expect { + t.Fatalf("expect %s but got %s", + hex.EncodeToString(expect[:]), + hex.EncodeToString(got[:])) + } + } +}