diff --git a/go.mod b/go.mod index 015bfe480..e086ef3b2 100644 --- a/go.mod +++ b/go.mod @@ -93,6 +93,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/ztyp v0.2.2 // indirect github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect github.com/valyala/fastrand v1.1.0 // indirect github.com/valyala/histogram v1.2.0 // indirect diff --git a/go.sum b/go.sum index 88a84b974..74e46b0e2 100644 --- a/go.sum +++ b/go.sum @@ -209,6 +209,7 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ 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/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= github.com/holiman/uint256 v1.2.1/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -342,6 +343,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/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= github.com/quasilyte/go-ruleguard/dsl v0.3.21/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= diff --git a/types/blob_txn.go b/types/blob_txn.go new file mode 100644 index 000000000..d9320541f --- /dev/null +++ b/types/blob_txn.go @@ -0,0 +1,229 @@ +package types + +// Minimal Blob Transaction parser for txpool purposes + +import ( + "fmt" + + "github.com/holiman/uint256" + "github.com/protolambda/ztyp/codec" + "github.com/protolambda/ztyp/view" +) + +const ( + FieldElementsPerBlob = 4096 +) + +type BlobTxNetworkWrapper struct { + Tx SignedBlobTx + BlobKZGs [][48]byte + Blobs [][FieldElementsPerBlob * 32]byte + KZGAggregatedProof [48]byte +} + +type SignedBlobTx struct { + Message BlobTx + Signature ECDSASignature +} + +func (sbtx SignedBlobTx) FixedLength() uint64 { return 0 } +func (sbtx *SignedBlobTx) Deserialize(dr *codec.DecodingReader) error { + err := dr.Container(&sbtx.Message, &sbtx.Signature) + if err != nil { + return fmt.Errorf("failed to deserialize SignedBlobTx: %w", err) + } + return nil +} + +type BlobTx struct { + ChainID uint256.Int + Nonce uint64 + MaxPriorityFeePerGas uint256.Int + MaxFeePerGas uint256.Int + Gas uint64 + Creation bool // true if To field is nil, indicating contract creation + Value uint256.Int + DataLen int // length of the Data in bytes + AccessListAddressCount int // number of addresses in access list + AccessListKeyCount int // number of storage keys in access list + + BlobVersionedHashes [][32]byte +} + +func (tx BlobTx) FixedLength() uint64 { return 0 } +func (tx *BlobTx) Deserialize(dr *codec.DecodingReader) error { + var chainID view.Uint256View + var nonce view.Uint64View + var maxPriorityFeePerGas view.Uint256View + var maxFeePerGas view.Uint256View + var gas view.Uint64View + var to addressView + var value view.Uint256View + var data dataView + var accessList accessListView + var maxFeePerDataGas view.Uint256View + var blobVersionedHashes blobVersionedHashesView + err := dr.Container(&chainID, &nonce, &maxPriorityFeePerGas, &maxFeePerGas, &gas, &to, &value, &data, &accessList, &maxFeePerDataGas, &blobVersionedHashes) + if err != nil { + return fmt.Errorf("failed to deserialize BlobTx: %w", err) + } + tx.ChainID = uint256.Int(chainID) + tx.Nonce = uint64(nonce) + tx.MaxPriorityFeePerGas = uint256.Int(maxPriorityFeePerGas) + tx.MaxFeePerGas = uint256.Int(maxFeePerGas) + tx.Gas = uint64(gas) + if !to.hasAddress { + tx.Creation = true + } + tx.Value = uint256.Int(value) + tx.DataLen = len(data) + tx.BlobVersionedHashes = blobVersionedHashes.hashes + tx.AccessListAddressCount = accessList.addresses + tx.AccessListKeyCount = accessList.keys + return nil +} + +// For deserializing To field +type addressView struct { + hasAddress bool +} + +func (av addressView) FixedLength() uint64 { return 0 } +func (av *addressView) Deserialize(dr *codec.DecodingReader) error { + len := dr.Scope() + b, err := dr.ReadByte() + if len == 1 { + if err != nil { + return err + } + if b != 0 { + return fmt.Errorf("expected 0 byte, got %v", b) + } + av.hasAddress = false + return nil + } + if len != 21 { + return fmt.Errorf("expected 1 or 21 bytes, got %v", len) + } + if b != 1 { + return fmt.Errorf("expected byte == 1, got %v", b) + } + av.hasAddress = true + dr.Skip(20) + return nil +} + +// For deserializing the Data field +type dataView []byte + +func (dv dataView) FixedLength() uint64 { return 0 } +func (dv *dataView) Deserialize(dr *codec.DecodingReader) error { + err := dr.ByteList((*[]byte)(dv), 1<<24 /*MAX_CALLDATA_SIZE*/) + if err != nil { + return fmt.Errorf("failed to deserialize dataView: %w", err) + } + return nil +} + +// For deserializing access list field +type accessListView struct { + addresses int + keys int +} + +func (alv accessListView) FixedLength() uint64 { return 0 } +func (alv *accessListView) Deserialize(dr *codec.DecodingReader) error { + // an access list is a list of access list tuples + tuples := []*tuple{} + add := func() codec.Deserializable { + alv.addresses++ + tuple := new(tuple) + tuples = append(tuples, tuple) + return tuple + } + err := dr.List(add, 0, 1<<24) + if err != nil { + return err + } + for i := range tuples { + alv.keys += int(*tuples[i]) + } + return nil +} + +type tuple int // count of keys in the access list tuple + +func (t tuple) FixedLength() uint64 { return 0 } +func (t *tuple) Deserialize(dr *codec.DecodingReader) error { + // an access list tuple consists of 20 bytes for the address, and then 4 bytes for the + // "offset", followed by the list of 32-byte storage keys. + scope := dr.Scope() + if scope < 24 { + return fmt.Errorf("expected scope >= 24, got %v", scope) + } + // subtract address & offset + scope -= 24 + if scope%32 != 0 { + return fmt.Errorf("expected multiple of 32 bytes got: %v", scope) + } + length := scope / 32 + if length > 1<<24 { + return fmt.Errorf("too many storage keys: %v", length) + } + *t = tuple(length) + return nil +} + +type blobVersionedHashesView struct { + hashes [][32]byte +} + +func (b blobVersionedHashesView) FixedLength() uint64 { return 0 } +func (b *blobVersionedHashesView) Deserialize(dr *codec.DecodingReader) error { + b.hashes = nil + scope := dr.Scope() + if scope == 0 { + return nil + } + if scope%32 != 0 { + return fmt.Errorf("scope not a multiple of 32. got: %v", scope) + } + length := scope / 32 + if length > 1<<24 /*MAX_VERSIONED_HASHES_LIST_SIZE*/ { + return fmt.Errorf("access list too long: %v", length) + } + hashes := make([]byte, scope) + _, err := dr.Read(hashes) + if err != nil { + return err + } + b.hashes = make([][32]byte, length) + for i := 0; i < int(length); i++ { + copy(b.hashes[i][:], hashes[i*32:i*32+32]) + } + return nil +} + +type ECDSASignature struct { + V byte + R [32]byte + S [32]byte +} + +func (sig ECDSASignature) FixedLength() uint64 { return 1 + 32 + 32 } +func (sig *ECDSASignature) Deserialize(dr *codec.DecodingReader) error { + len := sig.FixedLength() + scope := dr.Scope() + if scope != len { + return fmt.Errorf("failed to decode signature: expected %v bytes got %v", len, scope) + } + data := make([]byte, len) + _, err := dr.Read(data) + if err != nil { + return err + } + sig.V = data[0] + copy(sig.R[:], data[1:33]) + copy(sig.S[:], data[33:len]) + return nil +} diff --git a/types/blob_txn_test.go b/types/blob_txn_test.go new file mode 100644 index 000000000..e02aefaf9 --- /dev/null +++ b/types/blob_txn_test.go @@ -0,0 +1,124 @@ +package types + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/protolambda/ztyp/codec" +) + +var ( + // signedBlobTxHex is the SSZ encoding of the following tx: + // { + // "nonce": "0xa", + // "gasPrice": null, + // "maxPriorityFeePerGas": "0x2a", + // "maxFeePerGas": "0xa", + // "gas": "0x1e241", + // "value": "0x64", + // "input": "0x616263646566", + // "v": "0x1", + // "r": "0xe995f26f2f424703e00ef9c9709248dc6587f3045e2dd536eedf96651a4b680d", + // "s": "0x13836dded49612eb61c61e9c61aa343a26f4ba37b5d53da3b6d9326b64a09668", + // "to": "0x095e7baea6a6c7c4c2dfeb977efac326af552d87", + // "chainId": "0x1", + // "accessList": [ + // { + // "address": "0x0000000000000000000000000000000000000001", + // "storageKeys": [ + // "0x0000000000000000000000000000000000000000000000000000000000000000", + // "0x0100000000000000000000000000000000000000000000000000000000000000" + // ] + // }, + // { + // "address": "0x0000000000000000000000000000000000000002", + // "storageKeys": [ + // "0x0200000000000000000000000000000000000000000000000000000000000000" + // ] + // } + // ], + // "maxFeePerDataGas": "0x0", + // "blobVersionedHashes": [ + // "0x010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c444014", + // "0x00000000000000000000000000000000000000000000000000000000deadbeef" + // ], + // "kzgAggregatedProof": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + // "hash": "0xabfab29ef05293b52e448f5e85eae4a99c1496cdf59f59987a37ba90912c8801" + // } + + signedBlobTxHex = "45000000010d684b1a6596dfee36d52d5e04f38765dc489270c9f90ee00347422f6ff295e96896a0646b32d9b6a33dd5b537baf4263a34aa619c1ec661eb1296d4de6d831301000000000000000000000000000000000000000000000000000000000000000a000000000000002a000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000041e2010000000000c00000006400000000000000000000000000000000000000000000000000000000000000d5000000db00000000000000000000000000000000000000000000000000000000000000000000007301000001095e7baea6a6c7c4c2dfeb977efac326af552d876162636465660800000060000000000000000000000000000000000000000000000118000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002180000000200000000000000000000000000000000000000000000000000000000000000010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c44401400000000000000000000000000000000000000000000000000000000deadbeef" + + // Same as above only with nil To field to test contract creation indicator + signedBlobTxNoRecipientHex = "45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a000000000000002a000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000041e2010000000000c00000006400000000000000000000000000000000000000000000000000000000000000c1000000c700000000000000000000000000000000000000000000000000000000000000000000005f010000006162636465660800000060000000000000000000000000000000000000000000000118000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002180000000200000000000000000000000000000000000000000000000000000000000000010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c44401400000000000000000000000000000000000000000000000000000000deadbeef" +) + +func signedBlobTxFromHex(hexStr string) (*SignedBlobTx, error) { + txBytes, err := hex.DecodeString(hexStr) + if err != nil { + return nil, err + } + tx := SignedBlobTx{} + buf := bytes.NewReader(txBytes) + dr := codec.NewDecodingReader(buf, uint64(len(txBytes))) + err = tx.Deserialize(dr) + if err != nil { + return nil, err + } + return &tx, nil +} + +func TestParseSignedBlobTx(t *testing.T) { + tx, err := signedBlobTxFromHex(signedBlobTxHex) + if err != nil { + t.Fatalf("couldn't create test case: %v", err) + } + msg := tx.Message + if msg.ChainID.Uint64() != 1 { + t.Errorf("Expected chain id == 1, got: %v", msg.ChainID.Uint64()) + } + if msg.Nonce != 10 { + t.Errorf("Expected nonce == 10, got: %v", msg.Nonce) + } + if msg.MaxPriorityFeePerGas.Uint64() != 42 { + t.Errorf("Expected MaxPriorityFeePerGas == 42, got %v", msg.MaxPriorityFeePerGas.Uint64()) + } + if msg.MaxFeePerGas.Uint64() != 10 { + t.Errorf("Expected MaxFeePerGas == 10, got %v", msg.MaxFeePerGas.Uint64()) + } + if msg.Gas != 123457 { + t.Errorf("Expected Gas == 123457, got %v", msg.Gas) + } + if msg.Creation == true { + t.Errorf("Expected !msg.Creation") + } + if msg.Value.Uint64() != 100 { + t.Errorf("Expected msg.Value == 100, got %v", msg.Value.Uint64()) + } + if msg.DataLen != 6 { + t.Errorf("Expected DataLen == 6, got %v", msg.DataLen) + } + if len(msg.BlobVersionedHashes) != 2 { + t.Errorf("Expected 2 blob hashes, got %v", len(msg.BlobVersionedHashes)) + } + if msg.AccessListAddressCount != 2 { + t.Errorf("Expected 2 addresses in access list, got %v", msg.AccessListAddressCount) + } + if msg.AccessListKeyCount != 3 { + t.Errorf("Expected 3 keys in access list, got %v", msg.AccessListKeyCount) + } + + sig := tx.Signature + if sig.V != 1 { + t.Errorf("Expected sig.V == 1, got %v", sig.V) + } + + // Test "Creation == true" + tx, err = signedBlobTxFromHex(signedBlobTxNoRecipientHex) + if err != nil { + t.Fatalf("couldn't create test case: %v", err) + } + if tx.Message.Creation == false { + t.Errorf("Expected msg.Creation") + } +}