[NOD-172] Port ECMH from bchd and fix Remove to preserve commutativity (#292)

* [NOD-172] Port EMCH from bchd

* [NOD-172] Fix hdkeychain.TestErrors and add btcec.TestRecoverCompact

* [NOD-172] Make ECMH immutable

* [NOD-172] Fix gofmt errors

* [NOD-172] Add TestMultiset_NewMultisetFromDataSlice and fix Point to be immutable

* [NOD-172] Fix gofmt errors

* [NOD-172] Add test for checking that the Union of a multiset and its inverse is zero
This commit is contained in:
Ori Newman 2019-05-15 16:07:37 +03:00 committed by Svarog
parent 8dedca693e
commit 5c5491e1e4
5 changed files with 438 additions and 3 deletions

150
btcec/ecmh.go Normal file
View File

@ -0,0 +1,150 @@
package btcec
import (
"crypto/sha256"
"encoding/binary"
"math/big"
"github.com/daglabs/btcd/util/daghash"
)
// Multiset tracks the state of a multiset as used to calculate the ECMH
// (elliptic curve multiset hash) hash of an unordered set. The state is
// a point on the curve. New elements are hashed onto a point on the curve
// and then added to the current state. Hence elements can be added in any
// order and we can also remove elements to return to a prior hash.
type Multiset struct {
curve *KoblitzCurve
x *big.Int
y *big.Int
}
// NewMultiset returns an empty multiset. The hash of an empty set
// is the 32 byte value of zero.
func NewMultiset(curve *KoblitzCurve) *Multiset {
return &Multiset{curve: curve, x: big.NewInt(0), y: big.NewInt(0)}
}
// NewMultisetFromPoint initializes a new multiset with the given x, y
// coordinate.
func NewMultisetFromPoint(curve *KoblitzCurve, x, y *big.Int) *Multiset {
var copyX, copyY big.Int
if x != nil {
copyX.Set(x)
}
if y != nil {
copyY.Set(y)
}
return &Multiset{curve: curve, x: &copyX, y: &copyY}
}
// NewMultisetFromDataSlice gets a curve and a slice of byte
// slices, creates an empty multiset, hashes each data and
// add it to the multiset, and return the resulting multiset.
func NewMultisetFromDataSlice(curve *KoblitzCurve, datas [][]byte) *Multiset {
ms := NewMultiset(curve)
for _, data := range datas {
x, y := hashToPoint(curve, data)
ms.addPoint(x, y)
}
return ms
}
func (ms *Multiset) clone() *Multiset {
return NewMultisetFromPoint(ms.curve, ms.x, ms.y)
}
// Add hashes the data onto the curve and returns
// a multiset with the new resulting point.
func (ms *Multiset) Add(data []byte) *Multiset {
newMs := ms.clone()
x, y := hashToPoint(ms.curve, data)
newMs.addPoint(x, y)
return newMs
}
func (ms *Multiset) addPoint(x, y *big.Int) {
ms.x, ms.y = ms.curve.Add(ms.x, ms.y, x, y)
}
// Remove hashes the data onto the curve, subtracts
// the point from the existing multiset, and returns
// a multiset with the new point. This function
// will execute regardless of whether or not the passed
// data was previously added to the set. Hence if you
// remove an element that was never added and also remove
// all the elements that were added, you will not get
// back to the point at infinity (empty set).
func (ms *Multiset) Remove(data []byte) *Multiset {
newMs := ms.clone()
x, y := hashToPoint(ms.curve, data)
newMs.removePoint(x, y)
return newMs
}
func (ms *Multiset) removePoint(x, y *big.Int) {
y.Neg(y).Mod(y, ms.curve.P)
ms.x, ms.y = ms.curve.Add(ms.x, ms.y, x, y)
}
// Union will add the point of the passed multiset instance to the point
// of this multiset and will return a multiset with the resulting point.
func (ms *Multiset) Union(otherMultiset *Multiset) *Multiset {
newMs := ms.clone()
otherMsCopy := otherMultiset.clone()
newMs.addPoint(otherMsCopy.x, otherMsCopy.y)
return newMs
}
// Subtract will remove the point of the passed multiset instance from the point
// of this multiset and will return a multiset with the resulting point.
func (ms *Multiset) Subtract(otherMultiset *Multiset) *Multiset {
newMs := ms.clone()
otherMsCopy := otherMultiset.clone()
newMs.removePoint(otherMsCopy.x, otherMsCopy.y)
return newMs
}
// Hash serializes and returns the hash of the multiset. The hash of an empty
// set is the 32 byte value of zero. The hash of a non-empty multiset is the
// sha256 hash of the 32 byte x value concatenated with the 32 byte y value.
func (ms *Multiset) Hash() daghash.Hash {
if ms.x.Sign() == 0 && ms.y.Sign() == 0 {
return daghash.Hash{}
}
hash := sha256.Sum256(append(ms.x.Bytes(), ms.y.Bytes()...))
return daghash.Hash(hash)
}
// Point returns a copy of the x and y coordinates of the current multiset state.
func (ms *Multiset) Point() (x *big.Int, y *big.Int) {
var copyX, copyY big.Int
copyX.Set(ms.x)
copyY.Set(ms.y)
return &copyX, &copyY
}
// hashToPoint hashes the passed data into a point on the curve. The x value
// is sha256(n, sha256(data)) where n starts at zero. If the resulting x value
// is not in the field or x^3+7 is not quadratic residue then n is incremented
// and we try again. There is a 50% chance of success for any given iteration.
func hashToPoint(curve *KoblitzCurve, data []byte) (x *big.Int, y *big.Int) {
i := uint64(0)
var err error
h := sha256.Sum256(data)
n := make([]byte, 8)
for {
binary.LittleEndian.PutUint64(n, i)
h2 := sha256.Sum256(append(n, h[:]...))
x = new(big.Int).SetBytes(h2[:])
y, err = decompressPoint(curve, x, false)
if err == nil && x.Cmp(curve.N) < 0 {
break
}
i++
}
return x, y
}

213
btcec/ecmh_test.go Normal file
View File

@ -0,0 +1,213 @@
package btcec
import (
"bytes"
"encoding/hex"
"testing"
"github.com/daglabs/btcd/util/daghash"
)
var testVectors = []struct {
dataElementHex string
point [2]string
ecmhHash string
cumulativeHash string
}{
{
"982051fd1e4ba744bbbe680e1fee14677ba1a3c3540bf7b1cdb606e857233e0e00000000010000000100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac",
[2]string{"4f9a5dce69067bf28603e73a7af4c3650b16539b95bad05eee95dfc94d1efe2c", "346d5b777881f2729e7f89b2de4e8e79c7f2f42d1a0b25a8f10becb66e2d0f98"},
"9378d88aa60cfba3032cb19f27891886e26fc6de1afa340c1787a633591983f8",
"",
},
{
"d5fdcc541e25de1c7a5addedf24858b8bb665c9f36ef744ee42c316022c90f9b00000000020000000100f2052a010000004341047211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073dee6c89064984f03385237d92167c13e236446b417ab79a0fcae412ae3316b77ac",
[2]string{"68cf91eb2388a0287c13d46011c73fb8efb6be89c0867a47feccb2d11c390d2d", "f42ba72b1079d3d941881836f88b5dcd7c207a6a4839f129272c77ebb7194d42"},
"e2f3dc6f3aa867c50bd41b80aa3bdafcc9e1d13a6292ff8a5da95da123d185ef",
"afaa1f7ba0bd8a789422fdd6968639a4b8575baf7d54342a987073d038fdbafa",
},
{
"44f672226090d85db9a9f2fbfe5f0f9609b387af7be5b7fbb7a1767c831c9e9900000000030000000100f2052a0100000043410494b9d3e76c5b1629ecf97fff95d7a4bbdac87cc26099ada28066c6ff1eb9191223cd897194a08d0c2726c5747f1db49e8cf90e75dc3e3550ae9b30086f3cd5aaac",
[2]string{"359c6f59859d1d5af8e7081905cb6bb734c010be8680c14b5a89ee315694fc2b", "fb6ba531d4bd83b14c970ad1bec332a8ae9a05706cd5df7fd91a2f2cc32482fe"},
"ffed6804617a4a33b1037cdd26426e61fde0faa2c0cc045efffa17c00ff4adcf",
"e236a694532be6a4926ab8d5b1ff9cbfe638178e0008b0a8c5e87c3da2cdbc1c",
},
}
func TestHashToPoint(t *testing.T) {
for _, test := range testVectors {
data, err := hex.DecodeString(test.dataElementHex)
if err != nil {
t.Fatal(err)
}
x, y := hashToPoint(S256(), data)
if hex.EncodeToString(x.Bytes()) != test.point[0] || hex.EncodeToString(y.Bytes()) != test.point[1] {
t.Fatal("hashToPoint return incorrect point")
}
}
}
func TestMultiset_Hash(t *testing.T) {
for _, test := range testVectors {
data, err := hex.DecodeString(test.dataElementHex)
if err != nil {
t.Fatal(err)
}
x, y := hashToPoint(S256(), data)
m := NewMultisetFromPoint(S256(), x, y)
if m.Hash().String() != test.ecmhHash {
t.Fatal("Multiset-Hash returned incorrect hash serialization")
}
}
m := NewMultiset(S256())
emptySet := m.Hash()
zeroHash := daghash.Hash{}
if !bytes.Equal(emptySet[:], zeroHash[:]) {
t.Fatal("Empty set did not return zero hash")
}
}
func TestMultiset_AddRemove(t *testing.T) {
m := NewMultiset(S256())
for i, test := range testVectors {
data, err := hex.DecodeString(test.dataElementHex)
if err != nil {
t.Fatal(err)
}
m = m.Add(data)
if test.cumulativeHash != "" && m.Hash().String() != test.cumulativeHash {
t.Fatalf("Test #%d: Multiset-Add returned incorrect hash. Expected %s but got %s", i, test.cumulativeHash, m.Hash())
}
}
for i := len(testVectors) - 1; i > 0; i-- {
data, err := hex.DecodeString(testVectors[i].dataElementHex)
if err != nil {
t.Fatal(err)
}
m = m.Remove(data)
if testVectors[i-1].cumulativeHash != "" && m.Hash().String() != testVectors[i-1].cumulativeHash {
t.Fatalf("Test #%d: Multiset-Remove returned incorrect hash. Expected %s but got %s", i, testVectors[i].cumulativeHash, m.Hash())
}
}
}
func TestMultiset_UnionSubtract(t *testing.T) {
m1 := NewMultiset(S256())
zeroHash := m1.Hash().String()
for _, test := range testVectors {
data, err := hex.DecodeString(test.dataElementHex)
if err != nil {
t.Fatal(err)
}
m1 = m1.Add(data)
}
m2 := NewMultiset(S256())
for _, test := range testVectors {
data, err := hex.DecodeString(test.dataElementHex)
if err != nil {
t.Fatal(err)
}
m2 = m2.Remove(data)
}
m1m2Union := m1.Union(m2)
if m1m2Union.Hash().String() != zeroHash {
t.Fatalf("m1m2Union was expected to return to have zero hash, but was %s instead", m1m2Union.Hash())
}
m1Inverse := NewMultiset(S256()).Subtract(m1)
m1InverseM1Union := m1.Union(m1Inverse)
if m1InverseM1Union.Hash().String() != zeroHash {
t.Fatalf("m1InverseM1Union was expected to have zero hash, but got %s instead", m1InverseM1Union.Hash())
}
m1SubtractM1 := m1.Subtract(m1)
if m1SubtractM1.Hash().String() != zeroHash {
t.Fatalf("m1SubtractM1 was expected to have zero hash, but got %s instead", m1SubtractM1.Hash())
}
}
func TestMultiset_Commutativity(t *testing.T) {
m := NewMultiset(S256())
zeroHash := m.Hash().String()
// Check that if we subtract values from zero and then re-add them, we return to zero.
for _, test := range testVectors {
data, err := hex.DecodeString(test.dataElementHex)
if err != nil {
t.Fatal(err)
}
m = m.Remove(data)
}
for _, test := range testVectors {
data, err := hex.DecodeString(test.dataElementHex)
if err != nil {
t.Fatal(err)
}
m = m.Add(data)
}
if m.Hash().String() != zeroHash {
t.Fatalf("m was expected to be zero hash, but was %s instead", m.Hash())
}
// Here we first remove an element from an empty multiset, and then add some other
// elements, and then we create a new empty multiset, then we add the same elements
// we added to the previous multiset, and then we remove the same element we remove
// the same element we removed from the previous multiset. According to commutativity
// laws, the result should be the same.
removeIndex := 0
removeData, err := hex.DecodeString(testVectors[removeIndex].dataElementHex)
if err != nil {
t.Fatal(err)
}
m1 := NewMultiset(S256())
m1 = m1.Remove(removeData)
for i, test := range testVectors {
if i != removeIndex {
data, err := hex.DecodeString(test.dataElementHex)
if err != nil {
t.Fatal(err)
}
m1 = m1.Add(data)
}
}
m2 := NewMultiset(S256())
for i, test := range testVectors {
if i != removeIndex {
data, err := hex.DecodeString(test.dataElementHex)
if err != nil {
t.Fatal(err)
}
m2 = m2.Add(data)
}
}
m2 = m2.Remove(removeData)
if m1.Hash().String() != m2.Hash().String() {
t.Fatalf("m1 and m2 was exepcted to have the same hash, but got instead m1 %s and m2 %s", m1.Hash(), m2.Hash())
}
}
func TestMultiset_NewMultisetFromDataSlice(t *testing.T) {
m1 := NewMultiset(S256())
datas := make([][]byte, 0, len(testVectors))
for _, test := range testVectors {
data, err := hex.DecodeString(test.dataElementHex)
if err != nil {
t.Fatal(err)
}
datas = append(datas, data)
m1 = m1.Add(data)
}
m2 := NewMultisetFromDataSlice(S256(), datas)
if m1.Hash().String() != m2.Hash().String() {
t.Fatalf("m1 and m2 was exepcted to have the same hash, but got instead m1 %s and m2 %s", m1.Hash(), m2.Hash())
}
}

View File

@ -32,13 +32,22 @@ func decompressPoint(curve *KoblitzCurve, x *big.Int, ybit bool) (*big.Int, erro
x3 := new(big.Int).Mul(x, x)
x3.Mul(x3, x)
x3.Add(x3, curve.Params().B)
x3.Mod(x3, curve.Params().P)
// now calculate sqrt mod p of x2 + B
// Now calculate sqrt mod p of x^3 + B
// This code used to do a full sqrt based on tonelli/shanks,
// but this was replaced by the algorithms referenced in
// https://bitcointalk.org/index.php?topic=162805.msg1712294#msg1712294
y := new(big.Int).Exp(x3, curve.QPlus1Div4(), curve.Params().P)
// Check that y is a square root of x^3 + B.
y2 := new(big.Int).Mul(y, y)
y2.Mod(y2, curve.Params().P)
if y2.Cmp(x3) != 0 {
return nil, fmt.Errorf("invalid square root")
}
// Verify that y-coord has expected parity.
if ybit != isOdd(y) {
y.Sub(curve.Params().P, y)
}

View File

@ -11,6 +11,7 @@ import (
"encoding/hex"
"fmt"
"math/big"
"reflect"
"testing"
)
@ -535,6 +536,67 @@ func TestSignCompact(t *testing.T) {
}
}
// recoveryTests assert basic tests for public key recovery from signatures.
// The cases are borrowed from github.com/fjl/btcec-issue.
var recoveryTests = []struct {
msg string
sig string
pub string
err error
}{
{
// Valid curve point recovered.
msg: "ce0677bb30baa8cf067c88db9811f4333d131bf8bcf12fe7065d211dce971008",
sig: "0190f27b8b488db00b00606796d2987f6a5f59ae62ea05effe84fef5b8b0e549984a691139ad57a3f0b906637673aa2f63d1f55cb1a69199d4009eea23ceaddc93",
pub: "04E32DF42865E97135ACFB65F3BAE71BDC86F4D49150AD6A440B6F15878109880A0A2B2667F7E725CEEA70C673093BF67663E0312623C8E091B13CF2C0F11EF652",
},
{
// Invalid curve point recovered.
msg: "00c547e4f7b0f325ad1e56f57e26c745b09a3e503d86e00e5255ff7f715d3d1c",
sig: "0100b1693892219d736caba55bdb67216e485557ea6b6af75f37096c9aa6a5a75f00b940b1d03b21e36b0e47e79769f095fe2ab855bd91e3a38756b7d75a9c4549",
err: fmt.Errorf("invalid square root"),
},
{
// Low R and S values.
msg: "ba09edc1275a285fb27bfe82c4eea240a907a0dbaf9e55764b8f318c37d5974f",
sig: "00000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000004",
pub: "04A7640409AA2083FDAD38B2D8DE1263B2251799591D840653FB02DBBA503D7745FCB83D80E08A1E02896BE691EA6AFFB8A35939A646F1FC79052A744B1C82EDC3",
},
}
func TestRecoverCompact(t *testing.T) {
for i, test := range recoveryTests {
msg := decodeHex(test.msg)
sig := decodeHex(test.sig)
// Magic DER constant.
sig[0] += 27
pub, _, err := RecoverCompact(S256(), sig, msg)
// Verify that returned error matches as expected.
if !reflect.DeepEqual(test.err, err) {
t.Errorf("unexpected error returned from pubkey "+
"recovery #%d: wanted %v, got %v",
i, test.err, err)
continue
}
// If check succeeded because a proper error was returned, we
// ignore the returned pubkey.
if err != nil {
continue
}
// Otherwise, ensure the correct public key was recovered.
exPub, _ := ParsePubKey(decodeHex(test.pub), S256())
if !exPub.IsEqual(pub) {
t.Errorf("unexpected recovered public key #%d: "+
"want %v, got %v", i, exPub, pub)
}
}
}
func TestRFC6979(t *testing.T) {
// Test vectors matching Trezor and CoreBitcoin implementations.
// - https://github.com/trezor/trezor-crypto/blob/9fea8f8ab377dc514e40c6fd1f7c89a74c1d8dc6/tests.c#L432-L453

View File

@ -12,10 +12,11 @@ import (
"bytes"
"encoding/hex"
"errors"
"github.com/daglabs/btcd/util"
"math"
"reflect"
"testing"
"github.com/daglabs/btcd/util"
)
// TestBIP0032Vectors tests the vectors provided by [BIP32] to ensure the
@ -856,7 +857,7 @@ func TestErrors(t *testing.T) {
{
name: "pubkey not on curve",
key: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ1hr9Rwbk95YadvBkQXxzHBSngB8ndpW6QH7zhhsXZ2jHyZqPjk",
err: errors.New("pubkey isn't on secp256k1 curve"),
err: errors.New("invalid square root"),
},
{
name: "unsupported version",