Commit afbe2e89 authored by Joshua Gutow's avatar Joshua Gutow Committed by GitHub

Merge pull request #8458 from testinprod-io/tip/span-batch-protected-tx-patch

op-node: Span batch Unprotected Tx Handling + Logic Error Fix
parents b669f328 6c316744
......@@ -19,7 +19,7 @@ import (
)
func RandomRawSpanBatch(rng *rand.Rand, chainId *big.Int) *RawSpanBatch {
blockCount := uint64(1 + rng.Int()&0xFF)
blockCount := uint64(4 + rng.Int()&0xFF) // at least 4
originBits := new(big.Int)
for i := 0; i < int(blockCount); i++ {
bit := uint(0)
......@@ -31,14 +31,24 @@ func RandomRawSpanBatch(rng *rand.Rand, chainId *big.Int) *RawSpanBatch {
var blockTxCounts []uint64
totalblockTxCounts := uint64(0)
for i := 0; i < int(blockCount); i++ {
blockTxCount := uint64(rng.Intn(16))
blockTxCount := 1 + uint64(rng.Intn(16))
blockTxCounts = append(blockTxCounts, blockTxCount)
totalblockTxCounts += blockTxCount
}
signer := types.NewLondonSigner(chainId)
londonSigner := types.NewLondonSigner(chainId)
var txs [][]byte
for i := 0; i < int(totalblockTxCounts); i++ {
tx := testutils.RandomTx(rng, new(big.Int).SetUint64(rng.Uint64()), signer)
var tx *types.Transaction
switch i % 4 {
case 0:
tx = testutils.RandomLegacyTx(rng, types.HomesteadSigner{})
case 1:
tx = testutils.RandomLegacyTx(rng, londonSigner)
case 2:
tx = testutils.RandomAccessListTx(rng, londonSigner)
case 3:
tx = testutils.RandomDynamicFeeTx(rng, londonSigner)
}
rawTx, err := tx.MarshalBinary()
if err != nil {
panic("MarshalBinary:" + err.Error())
......
......@@ -26,7 +26,7 @@ import (
// spanBatch := SpanBatchType ++ prefix ++ payload
// prefix := rel_timestamp ++ l1_origin_num ++ parent_check ++ l1_origin_check
// payload := block_count ++ origin_bits ++ block_tx_counts ++ txs
// txs := contract_creation_bits ++ y_parity_bits ++ tx_sigs ++ tx_tos ++ tx_datas ++ tx_nonces ++ tx_gases
// txs := contract_creation_bits ++ y_parity_bits ++ tx_sigs ++ tx_tos ++ tx_datas ++ tx_nonces ++ tx_gases ++ protected_bits
var ErrTooBigSpanBatchSize = errors.New("span batch size limit reached")
......@@ -41,7 +41,7 @@ type spanBatchPrefix struct {
type spanBatchPayload struct {
blockCount uint64 // Number of L2 block in the span
originBits *big.Int // Bitlist of blockCount bits. Each bit indicates if the L1 origin is changed at the L2 block.
originBits *big.Int // Standard span-batch bitlist of blockCount bits. Each bit indicates if the L1 origin is changed at the L2 block.
blockTxCounts []uint64 // List of transaction counts for each L2 block
txs *spanBatchTxs // Transactions encoded in SpanBatch specs
}
......@@ -58,34 +58,12 @@ func (b *RawSpanBatch) GetBatchType() int {
}
// decodeOriginBits parses data into bp.originBits
// originBits is bitlist right-padded to a multiple of 8 bits
func (bp *spanBatchPayload) decodeOriginBits(r *bytes.Reader) error {
originBitBufferLen := bp.blockCount / 8
if bp.blockCount%8 != 0 {
originBitBufferLen++
}
// avoid out of memory before allocation
if originBitBufferLen > MaxSpanBatchSize {
return ErrTooBigSpanBatchSize
}
originBitBuffer := make([]byte, originBitBufferLen)
_, err := io.ReadFull(r, originBitBuffer)
bits, err := decodeSpanBatchBits(r, bp.blockCount)
if err != nil {
return fmt.Errorf("failed to read origin bits: %w", err)
}
originBits := new(big.Int)
for i := 0; i < int(bp.blockCount); i += 8 {
end := i + 8
if end < int(bp.blockCount) {
end = int(bp.blockCount)
}
bits := originBitBuffer[i/8]
for j := i; j < end; j++ {
bit := uint((bits >> (j - i)) & 1)
originBits.SetBit(originBits, j, bit)
return fmt.Errorf("failed to decode origin bits: %w", err)
}
}
bp.originBits = originBits
bp.originBits = bits
return nil
}
......@@ -293,26 +271,9 @@ func (bp *spanBatchPrefix) encodePrefix(w io.Writer) error {
}
// encodeOriginBits encodes bp.originBits
// originBits is bitlist right-padded to a multiple of 8 bits
func (bp *spanBatchPayload) encodeOriginBits(w io.Writer) error {
originBitBufferLen := bp.blockCount / 8
if bp.blockCount%8 != 0 {
originBitBufferLen++
}
originBitBuffer := make([]byte, originBitBufferLen)
for i := 0; i < int(bp.blockCount); i += 8 {
end := i + 8
if end < int(bp.blockCount) {
end = int(bp.blockCount)
}
var bits uint = 0
for j := i; j < end; j++ {
bits |= bp.originBits.Bit(j) << (j - i)
}
originBitBuffer[i/8] = byte(bits)
}
if _, err := w.Write(originBitBuffer); err != nil {
return fmt.Errorf("cannot write origin bits: %w", err)
if err := encodeSpanBatchBits(w, bp.blockCount, bp.originBits); err != nil {
return fmt.Errorf("failed to encode origin bits: %w", err)
}
return nil
}
......
......@@ -447,9 +447,10 @@ func TestSpanBatchToSingularBatch(t *testing.T) {
func TestSpanBatchReadTxData(t *testing.T) {
cases := []spanBatchTxTest{
{"legacy tx", 32, testutils.RandomLegacyTx},
{"access list tx", 32, testutils.RandomAccessListTx},
{"dynamic fee tx", 32, testutils.RandomDynamicFeeTx},
{"unprotected legacy tx", 32, testutils.RandomLegacyTx, false},
{"legacy tx", 32, testutils.RandomLegacyTx, true},
{"access list tx", 32, testutils.RandomAccessListTx, true},
{"dynamic fee tx", 32, testutils.RandomDynamicFeeTx, true},
}
for i, testCase := range cases {
......@@ -457,6 +458,9 @@ func TestSpanBatchReadTxData(t *testing.T) {
rng := rand.New(rand.NewSource(int64(0x109550 + i)))
chainID := new(big.Int).SetUint64(rng.Uint64())
signer := types.NewLondonSigner(chainID)
if !testCase.protected {
signer = types.HomesteadSigner{}
}
var rawTxs [][]byte
var txs []*types.Transaction
......
......@@ -15,13 +15,15 @@ type spanBatchTxTest struct {
name string
trials int
mkTx func(rng *rand.Rand, signer types.Signer) *types.Transaction
protected bool
}
func TestSpanBatchTxConvert(t *testing.T) {
cases := []spanBatchTxTest{
{"legacy tx", 32, testutils.RandomLegacyTx},
{"access list tx", 32, testutils.RandomAccessListTx},
{"dynamic fee tx", 32, testutils.RandomDynamicFeeTx},
{"unprotected legacy tx", 32, testutils.RandomLegacyTx, false},
{"legacy tx", 32, testutils.RandomLegacyTx, true},
{"access list tx", 32, testutils.RandomAccessListTx, true},
{"dynamic fee tx", 32, testutils.RandomDynamicFeeTx, true},
}
for i, testCase := range cases {
......@@ -29,6 +31,9 @@ func TestSpanBatchTxConvert(t *testing.T) {
rng := rand.New(rand.NewSource(int64(0x1331 + i)))
chainID := big.NewInt(rng.Int63n(1000))
signer := types.NewLondonSigner(chainID)
if !testCase.protected {
signer = types.HomesteadSigner{}
}
for txIdx := 0; txIdx < testCase.trials; txIdx++ {
tx := testCase.mkTx(rng, signer)
......@@ -54,9 +59,10 @@ func TestSpanBatchTxConvert(t *testing.T) {
func TestSpanBatchTxRoundTrip(t *testing.T) {
cases := []spanBatchTxTest{
{"legacy tx", 32, testutils.RandomLegacyTx},
{"access list tx", 32, testutils.RandomAccessListTx},
{"dynamic fee tx", 32, testutils.RandomDynamicFeeTx},
{"unprotected legacy tx", 32, testutils.RandomLegacyTx, false},
{"legacy tx", 32, testutils.RandomLegacyTx, true},
{"access list tx", 32, testutils.RandomAccessListTx, true},
{"dynamic fee tx", 32, testutils.RandomDynamicFeeTx, true},
}
for i, testCase := range cases {
......@@ -64,6 +70,9 @@ func TestSpanBatchTxRoundTrip(t *testing.T) {
rng := rand.New(rand.NewSource(int64(0x1332 + i)))
chainID := big.NewInt(rng.Int63n(1000))
signer := types.NewLondonSigner(chainID)
if !testCase.protected {
signer = types.HomesteadSigner{}
}
for txIdx := 0; txIdx < testCase.trials; txIdx++ {
tx := testCase.mkTx(rng, signer)
......
......@@ -18,16 +18,19 @@ type spanBatchTxs struct {
// this field must be manually set
totalBlockTxCount uint64
// 7 fields
contractCreationBits *big.Int
yParityBits *big.Int
// 8 fields
contractCreationBits *big.Int // standard span-batch bitlist
yParityBits *big.Int // standard span-batch bitlist
txSigs []spanBatchSignature
txNonces []uint64
txGases []uint64
txTos []common.Address
txDatas []hexutil.Bytes
protectedBits *big.Int // standard span-batch bitlist
// intermediate variables which can be recovered
txTypes []int
totalLegacyTxCount uint64
}
type spanBatchSignature struct {
......@@ -36,58 +39,35 @@ type spanBatchSignature struct {
s *uint256.Int
}
// contractCreationBits is bitlist right-padded to a multiple of 8 bits
func (btx *spanBatchTxs) encodeContractCreationBits(w io.Writer) error {
contractCreationBitBufferLen := btx.totalBlockTxCount / 8
if btx.totalBlockTxCount%8 != 0 {
contractCreationBitBufferLen++
}
contractCreationBitBuffer := make([]byte, contractCreationBitBufferLen)
for i := 0; i < int(btx.totalBlockTxCount); i += 8 {
end := i + 8
if end < int(btx.totalBlockTxCount) {
end = int(btx.totalBlockTxCount)
}
var bits uint = 0
for j := i; j < end; j++ {
bits |= btx.contractCreationBits.Bit(j) << (j - i)
}
contractCreationBitBuffer[i/8] = byte(bits)
}
if _, err := w.Write(contractCreationBitBuffer); err != nil {
return fmt.Errorf("cannot write contract creation bits: %w", err)
if err := encodeSpanBatchBits(w, btx.totalBlockTxCount, btx.contractCreationBits); err != nil {
return fmt.Errorf("failed to encode contract creation bits: %w", err)
}
return nil
}
// contractCreationBits is bitlist right-padded to a multiple of 8 bits
func (btx *spanBatchTxs) decodeContractCreationBits(r *bytes.Reader) error {
contractCreationBitBufferLen := btx.totalBlockTxCount / 8
if btx.totalBlockTxCount%8 != 0 {
contractCreationBitBufferLen++
}
// avoid out of memory before allocation
if contractCreationBitBufferLen > MaxSpanBatchSize {
return ErrTooBigSpanBatchSize
}
contractCreationBitBuffer := make([]byte, contractCreationBitBufferLen)
_, err := io.ReadFull(r, contractCreationBitBuffer)
bits, err := decodeSpanBatchBits(r, btx.totalBlockTxCount)
if err != nil {
return fmt.Errorf("failed to read contract creation bits: %w", err)
return fmt.Errorf("failed to decode contract creation bits: %w", err)
}
contractCreationBits := new(big.Int)
for i := 0; i < int(btx.totalBlockTxCount); i += 8 {
end := i + 8
if end < int(btx.totalBlockTxCount) {
end = int(btx.totalBlockTxCount)
}
bits := contractCreationBitBuffer[i/8]
for j := i; j < end; j++ {
bit := uint((bits >> (j - i)) & 1)
contractCreationBits.SetBit(contractCreationBits, j, bit)
btx.contractCreationBits = bits
return nil
}
func (btx *spanBatchTxs) encodeProtectedBits(w io.Writer) error {
if err := encodeSpanBatchBits(w, btx.totalLegacyTxCount, btx.protectedBits); err != nil {
return fmt.Errorf("failed to encode protected bits: %w", err)
}
return nil
}
func (btx *spanBatchTxs) decodeProtectedBits(r *bytes.Reader) error {
bits, err := decodeSpanBatchBits(r, btx.totalLegacyTxCount)
if err != nil {
return fmt.Errorf("failed to decode protected bits: %w", err)
}
btx.contractCreationBits = contractCreationBits
btx.protectedBits = bits
return nil
}
......@@ -105,27 +85,19 @@ func (btx *spanBatchTxs) contractCreationCount() (uint64, error) {
return result, nil
}
// yParityBits is bitlist right-padded to a multiple of 8 bits
func (btx *spanBatchTxs) encodeYParityBits(w io.Writer) error {
yParityBitBufferLen := btx.totalBlockTxCount / 8
if btx.totalBlockTxCount%8 != 0 {
yParityBitBufferLen++
}
yParityBitBuffer := make([]byte, yParityBitBufferLen)
for i := 0; i < int(btx.totalBlockTxCount); i += 8 {
end := i + 8
if end < int(btx.totalBlockTxCount) {
end = int(btx.totalBlockTxCount)
}
var bits uint = 0
for j := i; j < end; j++ {
bits |= btx.yParityBits.Bit(j) << (j - i)
}
yParityBitBuffer[i/8] = byte(bits)
if err := encodeSpanBatchBits(w, btx.totalBlockTxCount, btx.yParityBits); err != nil {
return fmt.Errorf("failed to encode y-parity bits: %w", err)
}
if _, err := w.Write(yParityBitBuffer); err != nil {
return fmt.Errorf("cannot write y parity bits: %w", err)
return nil
}
func (btx *spanBatchTxs) decodeYParityBits(r *bytes.Reader) error {
bits, err := decodeSpanBatchBits(r, btx.totalBlockTxCount)
if err != nil {
return fmt.Errorf("failed to decode y-parity bits: %w", err)
}
btx.yParityBits = bits
return nil
}
......@@ -183,37 +155,6 @@ func (btx *spanBatchTxs) encodeTxDatas(w io.Writer) error {
return nil
}
// yParityBits is bitlist right-padded to a multiple of 8 bits
func (btx *spanBatchTxs) decodeYParityBits(r *bytes.Reader) error {
yParityBitBufferLen := btx.totalBlockTxCount / 8
if btx.totalBlockTxCount%8 != 0 {
yParityBitBufferLen++
}
// avoid out of memory before allocation
if yParityBitBufferLen > MaxSpanBatchSize {
return ErrTooBigSpanBatchSize
}
yParityBitBuffer := make([]byte, yParityBitBufferLen)
_, err := io.ReadFull(r, yParityBitBuffer)
if err != nil {
return fmt.Errorf("failed to read y parity bits: %w", err)
}
yParityBits := new(big.Int)
for i := 0; i < int(btx.totalBlockTxCount); i += 8 {
end := i + 8
if end < int(btx.totalBlockTxCount) {
end = int(btx.totalBlockTxCount)
}
bits := yParityBitBuffer[i/8]
for j := i; j < end; j++ {
bit := uint((bits >> (j - i)) & 1)
yParityBits.SetBit(yParityBits, j, bit)
}
}
btx.yParityBits = yParityBits
return nil
}
func (btx *spanBatchTxs) decodeTxSigsRS(r *bytes.Reader) error {
var txSigs []spanBatchSignature
var sigBuffer [32]byte
......@@ -290,6 +231,9 @@ func (btx *spanBatchTxs) decodeTxDatas(r *bytes.Reader) error {
}
txDatas = append(txDatas, txData)
txTypes = append(txTypes, txType)
if txType == types.LegacyTxType {
btx.totalLegacyTxCount++
}
}
btx.txDatas = txDatas
btx.txTypes = txTypes
......@@ -300,13 +244,23 @@ func (btx *spanBatchTxs) recoverV(chainID *big.Int) error {
if len(btx.txTypes) != len(btx.txSigs) {
return errors.New("tx type length and tx sigs length mismatch")
}
if btx.protectedBits == nil {
return errors.New("dev error: protected bits not set")
}
protectedBitsIdx := 0
for idx, txType := range btx.txTypes {
bit := uint64(btx.yParityBits.Bit(idx))
var v uint64
switch txType {
case types.LegacyTxType:
// EIP155
protectedBit := btx.protectedBits.Bit(protectedBitsIdx)
protectedBitsIdx++
if protectedBit == 0 {
v = 27 + bit
} else {
// EIP-155
v = chainID.Uint64()*2 + 35 + bit
}
case types.AccessListTxType:
v = bit
case types.DynamicFeeTxType:
......@@ -341,6 +295,9 @@ func (btx *spanBatchTxs) encode(w io.Writer) error {
if err := btx.encodeTxGases(w); err != nil {
return err
}
if err := btx.encodeProtectedBits(w); err != nil {
return err
}
return nil
}
......@@ -366,6 +323,9 @@ func (btx *spanBatchTxs) decode(r *bytes.Reader) error {
if err := btx.decodeTxGases(r); err != nil {
return err
}
if err := btx.decodeProtectedBits(r); err != nil {
return err
}
return nil
}
......@@ -408,9 +368,14 @@ func convertVToYParity(v uint64, txType int) (uint, error) {
var yParityBit uint
switch txType {
case types.LegacyTxType:
// EIP155: v = 2 * chainID + 35 + yParity
if isProtectedV(v, txType) {
// EIP-155: v = 2 * chainID + 35 + yParity
// v - 35 = yParity (mod 2)
yParityBit = uint((v - 35) & 1)
} else {
// unprotected legacy txs must have v = 27 or 28
yParityBit = uint(v - 27)
}
case types.AccessListTxType:
yParityBit = uint(v)
case types.DynamicFeeTxType:
......@@ -421,6 +386,15 @@ func convertVToYParity(v uint64, txType int) (uint, error) {
return yParityBit, nil
}
func isProtectedV(v uint64, txType int) bool {
if txType == types.LegacyTxType {
// if EIP-155 applied, v = 2 * chainID + 35 + yParity
return v != 27 && v != 28
}
// every non legacy tx are protected
return true
}
func newSpanBatchTxs(txs [][]byte, chainID *big.Int) (*spanBatchTxs, error) {
totalBlockTxCount := uint64(len(txs))
var txSigs []spanBatchSignature
......@@ -431,11 +405,21 @@ func newSpanBatchTxs(txs [][]byte, chainID *big.Int) (*spanBatchTxs, error) {
var txTypes []int
contractCreationBits := new(big.Int)
yParityBits := new(big.Int)
protectedBits := new(big.Int)
totalLegacyTxCount := uint64(0)
for idx := 0; idx < int(totalBlockTxCount); idx++ {
var tx types.Transaction
if err := tx.UnmarshalBinary(txs[idx]); err != nil {
return nil, errors.New("failed to decode tx")
}
if tx.Type() == types.LegacyTxType {
protectedBit := uint(0)
if tx.Protected() {
protectedBit = uint(1)
}
protectedBits.SetBit(protectedBits, int(totalLegacyTxCount), protectedBit)
totalLegacyTxCount++
}
if tx.Protected() && tx.ChainId().Cmp(chainID) != 0 {
return nil, fmt.Errorf("protected tx has chain ID %d, but expected chain ID %d", tx.ChainId(), chainID)
}
......@@ -481,5 +465,7 @@ func newSpanBatchTxs(txs [][]byte, chainID *big.Int) (*spanBatchTxs, error) {
txTos: txTos,
txDatas: txDatas,
txTypes: txTypes,
protectedBits: protectedBits,
totalLegacyTxCount: totalLegacyTxCount,
}, nil
}
......@@ -14,6 +14,12 @@ import (
"github.com/ethereum-optimism/optimism/op-service/testutils"
)
type txTypeTest struct {
name string
mkTx func(rng *rand.Rand, signer types.Signer) *types.Transaction
signer types.Signer
}
func TestSpanBatchTxsContractCreationBits(t *testing.T) {
rng := rand.New(rand.NewSource(0x1234567))
chainID := big.NewInt(rng.Int63n(1000))
......@@ -112,6 +118,44 @@ func TestSpanBatchTxsYParityBits(t *testing.T) {
require.Equal(t, yParityBits, sbt.yParityBits)
}
func TestSpanBatchTxsProtectedBits(t *testing.T) {
rng := rand.New(rand.NewSource(0x7331))
chainID := big.NewInt(rng.Int63n(1000))
rawSpanBatch := RandomRawSpanBatch(rng, chainID)
protectedBits := rawSpanBatch.txs.protectedBits
txTypes := rawSpanBatch.txs.txTypes
totalBlockTxCount := rawSpanBatch.txs.totalBlockTxCount
totalLegacyTxCount := rawSpanBatch.txs.totalLegacyTxCount
var sbt spanBatchTxs
sbt.protectedBits = protectedBits
sbt.totalBlockTxCount = totalBlockTxCount
sbt.txTypes = txTypes
sbt.totalLegacyTxCount = totalLegacyTxCount
var buf bytes.Buffer
err := sbt.encodeProtectedBits(&buf)
require.NoError(t, err)
// protectedBit field is fixed length: single bit
protectedBitBufferLen := totalLegacyTxCount / 8
require.NoError(t, err)
if totalLegacyTxCount%8 != 0 {
protectedBitBufferLen++
}
require.Equal(t, buf.Len(), int(protectedBitBufferLen))
result := buf.Bytes()
sbt.protectedBits = nil
r := bytes.NewReader(result)
err = sbt.decodeProtectedBits(r)
require.NoError(t, err)
require.Equal(t, protectedBits, sbt.protectedBits)
}
func TestSpanBatchTxsTxSigs(t *testing.T) {
rng := rand.New(rand.NewSource(0x73311337))
chainID := big.NewInt(rng.Int63n(1000))
......@@ -263,19 +307,39 @@ func TestSpanBatchTxsRecoverV(t *testing.T) {
rng := rand.New(rand.NewSource(0x123))
chainID := big.NewInt(rng.Int63n(1000))
signer := types.NewLondonSigner(chainID)
totalblockTxCount := rng.Intn(100)
londonSigner := types.NewLondonSigner(chainID)
totalblockTxCount := 20 + rng.Intn(100)
cases := []txTypeTest{
{"unprotected legacy tx", testutils.RandomLegacyTx, types.HomesteadSigner{}},
{"legacy tx", testutils.RandomLegacyTx, londonSigner},
{"access list tx", testutils.RandomAccessListTx, londonSigner},
{"dynamic fee tx", testutils.RandomDynamicFeeTx, londonSigner},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
var spanBatchTxs spanBatchTxs
var txTypes []int
var txSigs []spanBatchSignature
var originalVs []uint64
yParityBits := new(big.Int)
protectedBits := new(big.Int)
totalLegacyTxCount := 0
for idx := 0; idx < totalblockTxCount; idx++ {
tx := testutils.RandomTx(rng, new(big.Int).SetUint64(rng.Uint64()), signer)
txTypes = append(txTypes, int(tx.Type()))
tx := testCase.mkTx(rng, testCase.signer)
txType := tx.Type()
txTypes = append(txTypes, int(txType))
var txSig spanBatchSignature
v, r, s := tx.RawSignatureValues()
if txType == types.LegacyTxType {
protectedBit := uint(0)
if tx.Protected() {
protectedBit = uint(1)
}
protectedBits.SetBit(protectedBits, int(totalLegacyTxCount), protectedBit)
totalLegacyTxCount++
}
// Do not fill in txSig.V
txSig.r, _ = uint256.FromBig(r)
txSig.s, _ = uint256.FromBig(s)
......@@ -289,6 +353,7 @@ func TestSpanBatchTxsRecoverV(t *testing.T) {
spanBatchTxs.yParityBits = yParityBits
spanBatchTxs.txSigs = txSigs
spanBatchTxs.txTypes = txTypes
spanBatchTxs.protectedBits = protectedBits
// recover txSig.v
err := spanBatchTxs.recoverV(chainID)
require.NoError(t, err)
......@@ -297,8 +362,9 @@ func TestSpanBatchTxsRecoverV(t *testing.T) {
for _, txSig := range spanBatchTxs.txSigs {
recoveredVs = append(recoveredVs, txSig.v)
}
require.Equal(t, originalVs, recoveredVs, "recovered v mismatch")
})
}
}
func TestSpanBatchTxsRoundTrip(t *testing.T) {
......@@ -332,13 +398,22 @@ func TestSpanBatchTxsRoundTrip(t *testing.T) {
func TestSpanBatchTxsRoundTripFullTxs(t *testing.T) {
rng := rand.New(rand.NewSource(0x13377331))
chainID := big.NewInt(rng.Int63n(1000))
signer := types.NewLondonSigner(chainID)
londonSigner := types.NewLondonSigner(chainID)
cases := []txTypeTest{
{"unprotected legacy tx", testutils.RandomLegacyTx, types.HomesteadSigner{}},
{"legacy tx", testutils.RandomLegacyTx, londonSigner},
{"access list tx", testutils.RandomAccessListTx, londonSigner},
{"dynamic fee tx", testutils.RandomDynamicFeeTx, londonSigner},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
for i := 0; i < 4; i++ {
totalblockTxCounts := uint64(1 + rng.Int()&0xFF)
var txs [][]byte
for i := 0; i < int(totalblockTxCounts); i++ {
tx := testutils.RandomTx(rng, new(big.Int).SetUint64(rng.Uint64()), signer)
tx := testCase.mkTx(rng, testCase.signer)
rawTx, err := tx.MarshalBinary()
require.NoError(t, err)
txs = append(txs, rawTx)
......@@ -351,6 +426,8 @@ func TestSpanBatchTxsRoundTripFullTxs(t *testing.T) {
require.Equal(t, txs, txs2)
}
})
}
}
func TestSpanBatchTxsRecoverVInvalidTxType(t *testing.T) {
......@@ -362,6 +439,7 @@ func TestSpanBatchTxsRecoverVInvalidTxType(t *testing.T) {
sbt.txTypes = []int{types.DepositTxType}
sbt.txSigs = []spanBatchSignature{{v: 0, r: nil, s: nil}}
sbt.yParityBits = new(big.Int)
sbt.protectedBits = new(big.Int)
err := sbt.recoverV(chainID)
require.ErrorContains(t, err, "invalid tx type")
......@@ -370,12 +448,21 @@ func TestSpanBatchTxsRecoverVInvalidTxType(t *testing.T) {
func TestSpanBatchTxsFullTxNotEnoughTxTos(t *testing.T) {
rng := rand.New(rand.NewSource(0x13572468))
chainID := big.NewInt(rng.Int63n(1000))
signer := types.NewLondonSigner(chainID)
londonSigner := types.NewLondonSigner(chainID)
cases := []txTypeTest{
{"unprotected legacy tx", testutils.RandomLegacyTx, types.HomesteadSigner{}},
{"legacy tx", testutils.RandomLegacyTx, londonSigner},
{"access list tx", testutils.RandomAccessListTx, londonSigner},
{"dynamic fee tx", testutils.RandomDynamicFeeTx, londonSigner},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
totalblockTxCounts := uint64(1 + rng.Int()&0xFF)
var txs [][]byte
for i := 0; i < int(totalblockTxCounts); i++ {
tx := testutils.RandomTx(rng, new(big.Int).SetUint64(rng.Uint64()), signer)
tx := testCase.mkTx(rng, testCase.signer)
rawTx, err := tx.MarshalBinary()
require.NoError(t, err)
txs = append(txs, rawTx)
......@@ -388,6 +475,8 @@ func TestSpanBatchTxsFullTxNotEnoughTxTos(t *testing.T) {
_, err = sbt.fullTxs(chainID)
require.EqualError(t, err, "tx to not enough")
})
}
}
func TestSpanBatchTxsMaxContractCreationBitsLength(t *testing.T) {
......@@ -407,3 +496,13 @@ func TestSpanBatchTxsMaxYParityBitsLength(t *testing.T) {
err := sb.decodeOriginBits(r)
require.ErrorIs(t, err, ErrTooBigSpanBatchSize)
}
func TestSpanBatchTxsMaxProtectedBitsLength(t *testing.T) {
var sb RawSpanBatch
sb.txs = &spanBatchTxs{}
sb.txs.totalLegacyTxCount = 0xFFFFFFFFFFFFFFFF
r := bytes.NewReader([]byte{})
err := sb.txs.decodeProtectedBits(r)
require.ErrorIs(t, err, ErrTooBigSpanBatchSize)
}
package derive
import (
"bytes"
"fmt"
"io"
"math/big"
)
// decodeSpanBatchBits decodes a standard span-batch bitlist.
// The bitlist is encoded as big-endian integer, left-padded with zeroes to a multiple of 8 bits.
// The encoded bitlist cannot be longer than MaxSpanBatchSize.
func decodeSpanBatchBits(r *bytes.Reader, bitLength uint64) (*big.Int, error) {
// Round up, ensure enough bytes when number of bits is not a multiple of 8.
// Alternative of (L+7)/8 is not overflow-safe.
bufLen := bitLength / 8
if bitLength%8 != 0 {
bufLen++
}
// avoid out of memory before allocation
if bufLen > MaxSpanBatchSize {
return nil, ErrTooBigSpanBatchSize
}
buf := make([]byte, bufLen)
_, err := io.ReadFull(r, buf)
if err != nil {
return nil, fmt.Errorf("failed to read bits: %w", err)
}
out := new(big.Int)
out.SetBytes(buf)
// We read the correct number of bytes, but there may still be trailing bits
if l := uint64(out.BitLen()); l > bitLength {
return nil, fmt.Errorf("bitfield has %d bits, but expected no more than %d", l, bitLength)
}
return out, nil
}
// encodeSpanBatchBits encodes a standard span-batch bitlist.
// The bitlist is encoded as big-endian integer, left-padded with zeroes to a multiple of 8 bits.
// The encoded bitlist cannot be longer than MaxSpanBatchSize.
func encodeSpanBatchBits(w io.Writer, bitLength uint64, bits *big.Int) error {
if l := uint64(bits.BitLen()); l > bitLength {
return fmt.Errorf("bitfield is larger than bitLength: %d > %d", l, bitLength)
}
// Round up, ensure enough bytes when number of bits is not a multiple of 8.
// Alternative of (L+7)/8 is not overflow-safe.
bufLen := bitLength / 8
if bitLength%8 != 0 { // rounding up this way is safe against overflows
bufLen++
}
if bufLen > MaxSpanBatchSize {
return ErrTooBigSpanBatchSize
}
buf := make([]byte, bufLen)
bits.FillBytes(buf) // zero-extended, big-endian
if _, err := w.Write(buf); err != nil {
return fmt.Errorf("cannot write bits: %w", err)
}
return nil
}
......@@ -157,6 +157,10 @@ func RandomTx(rng *rand.Rand, baseFee *big.Int, signer types.Signer) *types.Tran
return tx
}
func RandomLegacyTxNotProtected(rng *rand.Rand) *types.Transaction {
return RandomLegacyTx(rng, types.HomesteadSigner{})
}
func RandomLegacyTx(rng *rand.Rand, signer types.Signer) *types.Transaction {
key := InsecureRandomKey(rng)
txData := &types.LegacyTx{
......
......@@ -16,7 +16,7 @@
- [`Chain ID` removal from initial specs](#chain-id-removal-from-initial-specs)
- [Reorganization of constant length transaction fields](#reorganization-of-constant-length-transaction-fields)
- [RLP encoding for only variable length fields](#rlp-encoding-for-only-variable-length-fields)
- [Store `y_parity` instead of `v`](#store-y_parity-instead-of-v)
- [Store `y_parity` and `protected_bit` instead of `v`](#store-y_parity-and-protected_bit-instead-of-v)
- [Adjust `txs` Data Layout for Better Compression](#adjust-txs-data-layout-for-better-compression)
- [`fee_recipients` Encoding Scheme](#fee_recipients-encoding-scheme)
- [How derivation works with Span Batch?](#how-derivation-works-with-span-batch)
......@@ -86,6 +86,9 @@ Notation:
[protobuf spec]: https://protobuf.dev/programming-guides/encoding/#varints
Standard bitlists, in the context of span-batches, are encoded as big-endian integers,
left-padded with zeroes to the next multiple of 8 bits.
Where:
- `prefix = rel_timestamp ++ l1_origin_num ++ parent_check ++ l1_origin_check`
......@@ -98,14 +101,15 @@ Where:
The hash is truncated to 20 bytes for efficiency, i.e. `span_end.l1_origin.hash[:20]`.
- `payload = block_count ++ origin_bits ++ block_tx_counts ++ txs`:
- `block_count`: `uvarint` number of L2 blocks. This is at least 1, empty span batches are invalid.
- `origin_bits`: bitlist of `block_count` bits, right-padded to a multiple of 8 bits:
- `origin_bits`: standard bitlist of `block_count` bits:
1 bit per L2 block, indicating if the L1 origin changed this L2 block.
- `block_tx_counts`: for each block, a `uvarint` of `len(block.transactions)`.
- `txs`: L2 transactions which is reorganized and encoded as below.
- `txs = contract_creation_bits ++ y_parity_bits ++ tx_sigs ++ tx_tos ++ tx_datas ++ tx_nonces ++ tx_gases`
- `contract_creation_bits`: bit list of `sum(block_tx_counts)` bits, right-padded to a multiple of 8 bits,
- `txs = contract_creation_bits ++ y_parity_bits ++
tx_sigs ++ tx_tos ++ tx_datas ++ tx_nonces ++ tx_gases ++ protected_bits`
- `contract_creation_bits`: standard bitlist of `sum(block_tx_counts)` bits:
1 bit per L2 transactions, indicating if transaction is a contract creation transaction.
- `y_parity_bits`: bit list of `sum(block_tx_counts)` bits, right-padded to a multiple of 8 bits,
- `y_parity_bits`: standard bitlist of `sum(block_tx_counts)` bits:
1 bit per L2 transactions, indicating the y parity value when recovering transaction sender address.
- `tx_sigs`: concatenated list of transaction signatures
- `r` is encoded as big-endian `uint256`
......@@ -121,6 +125,8 @@ Where:
- `legacy`: `gasLimit`
- `1`: ([EIP-2930]): `gasLimit`
- `2`: ([EIP-1559]): `gas_limit`
- `protected_bits`: standard bitlist of length of number of legacy transactions:
1 bit per L2 legacy transactions, indicating if transacion is protected([EIP-155]) or not.
Introduce version `2` to the [batch-format](./derivation.md#batch-format) table:
......@@ -147,6 +153,8 @@ Where:
[EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559
[EIP-155]: https://eips.ethereum.org/EIPS/eip-155
Total size of encoded span batch is limited to `MAX_SPAN_BATCH_SIZE` (currently 10,000,000 bytes,
equal to `MAX_RLP_BYTES_PER_CHANNEL`). Therefore every field size of span batch will be implicitly limited to
`MAX_SPAN_BATCH_SIZE` . There can be at least single span batch per channel, and channel size is limited
......@@ -201,10 +209,12 @@ Our goal is to find the sweet spot on code complexity - span batch size tradeoff
I decided that using RLP for all variable length fields will be the best option,
not risking codebase with gnarly custom encoding/decoding implementations.
### Store `y_parity` instead of `v`
### Store `y_parity` and `protected_bit` instead of `v`
For legacy type transactions, `v = 2 * ChainID + y_parity`. For other types of transactions, `v = y_parity`.
We may only store `y_parity`, which is single bit per L2 transaction.
Only legacy type transactions can be optionally protected. If protected([EIP-155]), `v = 2 * ChainID + 35 + y_parity`.
Else, `v = 27 + y_parity`. For other types of transactions, `v = y_parity`.
We store `y_parity`, which is single bit per L2 transaction.
We store `protected_bit`, which is single bit per L2 legacy type transactions to indicate that tx is protected.
This optimization will benefit more when ratio between number of legacy type transactions over number of transactions
excluding deposit tx is higher.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment