From b5711e2e8c62a93596b109f8d26fc7bf24231d96 Mon Sep 17 00:00:00 2001 From: Roberto Bayardo Date: Sat, 14 Jan 2023 12:11:59 -0800 Subject: [PATCH] add blob verification function, hash validation for blob tx parsing, and test tweaks --- go.mod | 3 +++ go.sum | 7 ++++++ types/blob_txn.go | 55 +++++++++++++++++++++++++++++++++++++++++- types/blob_txn_test.go | 21 ++++++++++++++++ types/txn.go | 9 +++++-- types/txn_test.go | 12 +++++---- 6 files changed, 99 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index e086ef3b2..a761dc81b 100644 --- a/go.mod +++ b/go.mod @@ -69,7 +69,9 @@ require ( github.com/golang/protobuf v1.5.2 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect + github.com/herumi/bls-eth-go-binary v1.28.1 // indirect github.com/huandu/xstrings v1.3.2 // indirect + github.com/kilic/bls12-381 v0.1.1-0.20220929213557-ca162e8a70f4 // indirect github.com/lispad/go-generics-tools v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect @@ -93,6 +95,7 @@ require ( github.com/pion/webrtc/v3 v3.1.42 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/protolambda/go-kzg v0.0.0-20221224134646-c91cee5e954e // indirect github.com/protolambda/ztyp v0.2.2 // indirect github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect github.com/valyala/fastrand v1.1.0 // indirect diff --git a/go.sum b/go.sum index 74e46b0e2..647b99b49 100644 --- a/go.sum +++ b/go.sum @@ -208,6 +208,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/herumi/bls-eth-go-binary v1.28.1 h1:fcIZ48y5EE9973k05XjE8+P3YiQgjZz4JI/YabAm8KA= +github.com/herumi/bls-eth-go-binary v1.28.1/go.mod h1:luAnRm3OsMQeokhGzpYmc0ZKwawY7o87PUEP11Z7r7U= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= github.com/holiman/uint256 v1.2.1 h1:XRtyuda/zw2l+Bq/38n5XUoEF72aSOu/77Thd9pPp2o= @@ -224,6 +226,8 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kilic/bls12-381 v0.1.1-0.20220929213557-ca162e8a70f4 h1:xWK4TZ4bRL05WQUU/3x6TG1l+IYAqdXpAeSLt/zZJc4= +github.com/kilic/bls12-381 v0.1.1-0.20220929213557-ca162e8a70f4/go.mod h1:tlkavyke+Ac7h8R3gZIjI5LKBcvMlSWnXNMgT3vZXo8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -343,6 +347,8 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/protolambda/go-kzg v0.0.0-20221224134646-c91cee5e954e h1:Wed8Zc7HuSVNDJQy6ybac5/1fpRoyhMDcPvGlMjyIiY= +github.com/protolambda/go-kzg v0.0.0-20221224134646-c91cee5e954e/go.mod h1:7EhkBJFo/qJ9sToiW5baPqbyPo/TadVHn4iNdpwEW/w= github.com/protolambda/ztyp v0.2.2 h1:rVcL3vBu9W/aV646zF6caLS/dyn9BN8NYiuJzicLNyY= github.com/protolambda/ztyp v0.2.2/go.mod h1:9bYgKGqg3wJqT9ac1gI2hnVb0STQq7p/1lapqrqY1dU= github.com/quasilyte/go-ruleguard/dsl v0.3.21 h1:vNkC6fC6qMLzCOGbnIHOd5ixUGgTbp3Z4fGnUgULlDA= @@ -492,6 +498,7 @@ golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/types/blob_txn.go b/types/blob_txn.go index 8592bced0..123a313bc 100644 --- a/types/blob_txn.go +++ b/types/blob_txn.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/holiman/uint256" + "github.com/protolambda/go-kzg/eth" "github.com/protolambda/ztyp/codec" "github.com/protolambda/ztyp/view" ) @@ -13,9 +14,13 @@ import ( const ( FieldElementsPerBlob = 4096 - BlobSize = FieldElementsPerBlob * 32 // blob size in bytes - each field element is 32 bytes + FieldElementSize = 32 + + BlobSize = FieldElementsPerBlob * FieldElementSize // blob size in bytes ProofSize = 48 // kzg proof size + + MaxBlobsPerBlock = 4 ) type BlobTxNetworkWrapper struct { @@ -303,3 +308,51 @@ func (sig *ECDSASignature) Deserialize(dr *codec.DecodingReader) error { copy(sig.S[:], data[33:len]) return nil } + +type blobSequence [][BlobSize]byte + +func (s blobSequence) At(i int) eth.Blob { return blob(s[i]) } +func (s blobSequence) Len() int { return len(s) } + +type blob [BlobSize]byte + +func (s blob) At(i int) [FieldElementSize]byte { + r := [FieldElementSize]byte{} + if i*FieldElementSize+FieldElementSize > len(s) { + return r + } + copy(r[:], s[i*FieldElementSize:i*FieldElementSize+FieldElementSize]) + return r +} +func (s blob) Len() int { return len(s) / FieldElementSize } + +type kzgSequence [][ProofSize]byte + +func (s kzgSequence) At(i int) eth.KZGCommitment { return eth.KZGCommitment(s[i]) } +func (s kzgSequence) Len() int { return len(s) } + +func VerifyBlobs(blobKZGs [][ProofSize]byte, blobs [][BlobSize]byte, kzgAggregatedProof [ProofSize]byte, blobVersionedHashes [][32]byte) error { + l1 := len(blobKZGs) + l2 := len(blobs) + l3 := len(blobVersionedHashes) + if l1 != l2 || l2 != l3 { + return fmt.Errorf("lengths don't match %v %v %v", l1, l2, l3) + } + if l1 > MaxBlobsPerBlock { + return fmt.Errorf("number of blobs exceeds max: %v", l1) + } + ok, err := eth.VerifyAggregateKZGProof(blobSequence(blobs), kzgSequence(blobKZGs), eth.KZGProof(kzgAggregatedProof)) + if err != nil { + return fmt.Errorf("error during proof verification: %v", err) + } + if !ok { + return fmt.Errorf("failed to verify kzg") + } + for i, h := range blobVersionedHashes { + computed := eth.KZGToVersionedHash(eth.KZGCommitment(blobKZGs[i])) + if computed != h { + return fmt.Errorf("versioned hash %d supposedly %s but does not match computed %s", i, h, computed) + } + } + return nil +} diff --git a/types/blob_txn_test.go b/types/blob_txn_test.go index 7cea15c12..a912a38c8 100644 --- a/types/blob_txn_test.go +++ b/types/blob_txn_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/protolambda/ztyp/codec" + "github.com/stretchr/testify/require" "github.com/ledgerwatch/erigon-lib/common" ) @@ -138,4 +139,24 @@ func TestParseBlobTxNetworkWrapper(t *testing.T) { if l1 != 2 || l2 != 2 || l3 != 2 { t.Errorf("Expected 2 each of kzgs / blobs / hashes, got: %v %v %v", l1, l2, l3) } + + // Test blob verification + err = VerifyBlobs(tx.BlobKZGs, tx.Blobs, tx.KZGAggregatedProof, tx.Tx.Message.BlobVersionedHashes) + require.NoError(t, err) + + // Mangle one byte in a blob and make sure verification fails + oldByte := tx.Blobs[0][10] + tx.Blobs[0][10] = 0 + err = VerifyBlobs(tx.BlobKZGs, tx.Blobs, tx.KZGAggregatedProof, tx.Tx.Message.BlobVersionedHashes) + if err == nil { + t.Fatal("Expected error when verifying invalid blob data, got none") + } + tx.Blobs[0][10] = oldByte + + // Mangle one byte in the proof and make sure verification failse + tx.KZGAggregatedProof[10] = 0 + err = VerifyBlobs(tx.BlobKZGs, tx.Blobs, tx.KZGAggregatedProof, tx.Tx.Message.BlobVersionedHashes) + if err == nil { + t.Fatal("Expected error when verifying invalid aggregated proof, got none") + } } diff --git a/types/txn.go b/types/txn.go index 05d64f2a4..5ec4c471e 100644 --- a/types/txn.go +++ b/types/txn.go @@ -160,7 +160,7 @@ func (ctx *TxParseContext) ParseTransaction(payload []byte, pos int, slot *TxSlo // since it is SSZ format. We assume the scope ends at the last byte of the payload // argument; this currently seems to always be the case, but does not seem to be // mandated by this function's contract. - return len(payload), ctx.ParseBlobTransaction(payload[p:], slot, sender) + return len(payload), ctx.ParseBlobTransaction(payload[p:], slot, sender, validateHash) } if _, err = ctx.Keccak1.Write(payload[p : p+1]); err != nil { return 0, fmt.Errorf("%w: computing IdHash (hashing type Prefix): %s", ErrParseTxn, err) @@ -480,10 +480,15 @@ func (ctx *TxParseContext) ParseTransaction(payload []byte, pos int, slot *TxSlo return p, nil } -func (ctx *TxParseContext) ParseBlobTransaction(payload []byte, slot *TxSlot, sender []byte) error { +func (ctx *TxParseContext) ParseBlobTransaction(payload []byte, slot *TxSlot, sender []byte, validateHash func([]byte) error) error { slot.Rlp = payload // includes type byte ctx.Keccak1.Write(payload) ctx.Keccak1.(io.Reader).Read(slot.IDHash[:32]) + if validateHash != nil { + if err := validateHash(slot.IDHash[:32]); err != nil { + return err + } + } payload = payload[1:] // payload should now include the SSZ encoded tx and nothing more diff --git a/types/txn_test.go b/types/txn_test.go index 58f5488c2..f9a200db4 100644 --- a/types/txn_test.go +++ b/types/txn_test.go @@ -35,17 +35,19 @@ func TestParseBlobTransaction(t *testing.T) { ctx := NewTxParseContext(*new(uint256.Int).SetUint64(1)) tx, txSender := &TxSlot{}, [20]byte{} - // Prepare the payload, which is an RLP encoded string consisting of the blob tx type byte - // followed by the ssz encoded transaction data. ssz := common.MustDecodeHex(strings.TrimSpace(blobTxNetworkWrapperHex)) - stringLen := rlp.StringLen(len(ssz) + 1) - payload := make([]byte, stringLen) - rlp.EncodeString(append([]byte{byte(BlobTxType)}, ssz...), payload) + payload := append([]byte{byte(BlobTxType)}, ssz...) parseEnd, err := ctx.ParseTransaction(payload, 0, tx, txSender[:], false /* hasEnvelope */, nil) require.NoError(err) require.Equal(len(payload), parseEnd) + // Same test only with an rlp tx envelope. + stringLen := rlp.StringLen(len(ssz) + 1) + payload = make([]byte, stringLen) + rlp.EncodeString(append([]byte{byte(BlobTxType)}, ssz...), payload) + parseEnd, err = ctx.ParseTransaction(payload, 0, tx, txSender[:], true /* hasEnvelope */, nil) + // TODO: test that tx fields are set properly. Need a better input transaction as this example // has zeros for all fields other than the blobs. }