package shreder import ( "encoding/binary" "fmt" "io" ) type constArray struct { data []byte size int offset int } func newConstArray(data []byte) constArray { return constArray{ data: data, size: len(data), offset: 0, } } func (c *constArray) Len() int { return c.size } func (c *constArray) Offset() int { return c.offset } func (c *constArray) Read(cap int) ([]byte, error) { if c.offset+cap > c.size { return nil, io.EOF } c.offset += cap return c.data[c.offset-cap : c.offset], nil } func (c *constArray) ReadBytes() (byte, error) { if c.offset >= c.size { return 0, io.EOF } c.offset++ return c.data[c.offset-1], nil } func (c *constArray) PeekBytes() (byte, error) { if c.offset >= c.size { return 0, io.EOF } return c.data[c.offset], nil } func (c *constArray) ReadU64() (uint64, error) { if c.offset+8 > c.size { return 0, io.EOF } c.offset += 8 return binary.LittleEndian.Uint64(c.data[c.offset-8 : c.offset]), nil } func (c *constArray) ReadCompactU16() (uint16, error) { ln := 0 size := 0 for i := 0; i < 3; i++ { if len(c.data[c.offset:]) == 0 { return 0, fmt.Errorf("unable to decode compact u16 at %d: zero byte", i) } elem := int(c.data[c.offset+i]) if elem == 0 && i != 0 { return 0, fmt.Errorf("alias") } if i == 2 && (elem&0x80) != 0 { return 0, fmt.Errorf("byte three continues") } ln |= (elem & 0x7f) << (size * 7) size++ if (elem & 0x80) == 0 { break } } c.offset += size return uint16(ln), nil } func (c *constArray) Skip(size int) error { if c.offset+size > c.size { return io.EOF } c.offset += size return nil } // entriesToVersionedTransaction converts raw entry bytes to versioned transactions. func entriesToVersionedTransaction(slot uint64, b constArray) ([]*versionedTransaction, error) { b.offset = 0 entriesNum, err := b.ReadU64() if err != nil { return nil, fmt.Errorf("unable to read entries num: %w", err) } if entriesNum == 0 { return nil, nil } //preCap := b.Len() / 256 //if preCap < int(entriesNum) { // preCap = int(entriesNum) //} vs := make([]*versionedTransaction, 0, entriesNum) // logger.Debug("parsing entries", "count", entriesNum, "data len", b.Len(), "data", base64.StdEncoding.EncodeToString(b.data)) for i := uint64(0); i < entriesNum; i++ { err = b.Skip(40) if err != nil { return vs, fmt.Errorf("failed to skip num_hashes + hash of entry %d: %w", i, err) } numTx, err := b.ReadU64() if err != nil { return vs, fmt.Errorf("failed to read num_transactions of entry %d: %w", i, err) } for j := 0; j < int(numTx); j++ { numSignatures, err := b.ReadCompactU16() if err != nil { return vs, fmt.Errorf("failed to read numSignatures in entry %d, txn %d: %w", i, j, err) } // todo : enforce a maximum number of signatures to prevent OOM if numSignatures > 16 { return vs, fmt.Errorf("numSignatures %d exceeds maximum in entry %d, txn %d", numSignatures, i, j) } if numSignatures == 0 { return vs, fmt.Errorf("numSignatures is zero in entry %d, txn %d", i, j) } versioned := requireVersionedPool() // get a versioned transaction from the pool vs = append(vs, versioned) versioned.block = slot versioned.bindArray = b.data versioned.signatures = int(numSignatures) versioned.signaturesOffset = b.Offset() err = b.Skip(64 * int(numSignatures)) if err != nil { return vs, fmt.Errorf("unable to read signature in entry %d, txn %d: %w", i, j, err) } msgVersion, err := b.PeekBytes() if err != nil { return vs, fmt.Errorf("unable to read message version in entry %d, txn %d: %w", i, j, err) } msgVersion = msgVersion & 0x80 >> 7 // mask to get only the version bits legacy := msgVersion == 0 headerSkip := 3 if !legacy { headerSkip = 4 } // skip msg version, mx.Header+3 err = b.Skip(headerSkip) if err != nil { return vs, fmt.Errorf("unable to skip message header in entry %d, txn %d: %w", i, j, err) } // read mx.AccountKeys // _, err = r.Read(u16[:]) numAccountKeys, err := b.ReadCompactU16() // logger.Info("tx", "hash", versioned.Signatures[0].String(), "version", msgVersion) if err != nil { return vs, fmt.Errorf("unable to decode numAccountKeys in entry %d, txn %d: %w", i, j, err) } // todo : enforce a maximum number of account keys to prevent OOM if numAccountKeys > 255 { return vs, fmt.Errorf("numAccountKeys %d exceeds maximum in entry %d, txn %d", numAccountKeys, i, j) } versioned.staticAccountKeys = uint8(numAccountKeys) versioned.staticAccountKeysOffset = b.Offset() err = b.Skip(32 * int(numAccountKeys)) if err != nil { return vs, fmt.Errorf("unable to read accountKey in entry %d, txn %d: %w", i, j, err) } //skip solana hash err = b.Skip(32) if err != nil { return vs, fmt.Errorf("unable to skip recentBlockhash in entry %d, txn %d: %w", i, j, err) } // read mx.Instructions numInstructions, err := b.ReadCompactU16() if err != nil { return vs, fmt.Errorf("unable to decode numInstructions in entry %d, txn %d: %w", i, j, err) } // todo : enforce a maximum number of instructions to prevent OOM if numInstructions >= 256 { return vs, fmt.Errorf("numInstructions %d exceeds maximum in entry %d, txn %d, txHash: %s", numInstructions, i, j, versioned.Signatures()) } versioned.instructions = int(numInstructions) if cap(versioned.Instrs) < int(numInstructions) { versioned.Instrs = make([]compiledInstruction, numInstructions) } else { versioned.Instrs = versioned.Instrs[:numInstructions] } for k := 0; k < int(numInstructions); k++ { versioned.Instrs[k].ProgramIDIndex, err = b.ReadBytes() if err != nil { return vs, fmt.Errorf("unable to read mx.Instructions[%d].ProgramIDIndex in entry %d, txn %d: %w", k, i, j, err) } numAccounts, err := b.ReadCompactU16() if err != nil { return vs, fmt.Errorf("unable to decode numAccounts for ix[%d] in entry %d, txn %d: %w", k, i, j, err) } // todo : enforce a maximum number of accounts to prevent OOM if numAccounts >= 256 { return vs, fmt.Errorf("numAccounts %d exceeds maximum for ix[%d] in entry %d, txn %d", numAccounts, k, i, j) } versioned.Instrs[k].AccountsLen = int(numAccounts) if numAccounts != 0 { versioned.Instrs[k].AccountsOffset = b.Offset() err = b.Skip(int(numAccounts)) if err != nil { return vs, fmt.Errorf("unable to read mx.Instructions[%d].Accounts in entry %d, txn %d: %w", k, i, j, err) } } // _, err = r.Read(u16[:]) dataLen, err := b.ReadCompactU16() if err != nil { return vs, fmt.Errorf("unable to decode mx.Instructions[%d].Data length in entry %d, txn %d: %w", k, i, j, err) } //todo : enforce a maximum data length to prevent OOM if dataLen > 2048 { return vs, fmt.Errorf("mx.Instructions[%d].Data length %d exceeds maximum in entry %d, txn %d, txHash: %s", k, dataLen, i, j, versioned.Signatures()) } versioned.Instrs[k].DataLen = int(dataLen) if dataLen > 0 { versioned.Instrs[k].DataOffset = b.Offset() err = b.Skip(int(dataLen)) if err != nil { return vs, fmt.Errorf("unable to read mx.Instructions[%d].Data in entry %d, txn %d: %w", k, i, j, err) } } } if !legacy { // read mx.AddressTableLookups numLookups, err := b.ReadBytes() if err != nil { return vs, fmt.Errorf("unable to read numAddressTableLookups in entry %d, txn %d: %w", i, j, err) } if cap(versioned.ATL) < int(numLookups) { versioned.ATL = make([]addressTableLookup, numLookups) } else { versioned.ATL = versioned.ATL[:numLookups] } versioned.addressTableLookups = int(numLookups) for k := uint8(0); k < numLookups; k++ { versioned.ATL[k].AccountKeyOffset = b.Offset() err = b.Skip(32) if err != nil { return vs, fmt.Errorf("unable to read address table account key for lookup[%d] in entry %d, txn %d: %w", k, i, j, err) } numWritable, err := b.ReadCompactU16() if err != nil { return vs, fmt.Errorf("unable to decode numWritableIndexes for lookup[%d] in entry %d, txn %d: %w", k, i, j, err) } // todo : enforce a maximum number of writable indexes to prevent OOM if numWritable >= 256 { return vs, fmt.Errorf("numWritableIndexes %d exceeds maximum for lookup[%d] in entry %d, txn %d", numWritable, k, i, j) } versioned.ATL[k].WriteLen = int(numWritable) if numWritable > 0 { versioned.ATL[k].WriteOffset = b.Offset() err = b.Skip(int(numWritable)) if err != nil { return vs, fmt.Errorf("unable to read writableIndexes for lookup[%d] in entry %d, txn %d: %w", k, i, j, err) } } // _, err = r.Read(u16[:]) numReadonly, err := b.ReadCompactU16() if err != nil { return vs, fmt.Errorf("unable to decode numReadonlyIndexes for lookup[%d] in entry %d, txn %d: %w", k, i, j, err) } // todo : enforce a maximum number of readonly indexes to prevent OOM if numReadonly > 256 { return vs, fmt.Errorf("numReadonlyIndexes %d exceeds maximum for lookup[%d] in entry %d, txn %d", numReadonly, k, i, j) } versioned.ATL[k].ReadLen = int(numReadonly) if numReadonly > 0 { versioned.ATL[k].ReadOffset = b.Offset() err = b.Skip(int(numReadonly)) if err != nil { return vs, fmt.Errorf("unable to read readonlyIndexes for lookup[%d] in entry %d, txn %d: %w", k, i, j, err) } } } } } } // logger.Debug("parsing num_transactions of entry", "slot", slot, "count", entriesNum, "data len", b.Len(), "num_tx", uint32(len(vs))) //logger.Debug("parsing entries", "slot", slot, "count", entriesNum, "data len", b.Len(), "txns", len(vs)) return vs, nil } func decodeCompactU16(b []byte) (int, uint16, error) { ln := 0 size := 0 for i := 0; i < 3; i++ { if len(b) == 0 { return 0, 0, fmt.Errorf("unable to decode compact u16 at %d: zero byte", i) } elem := int(b[0]) b = b[1:] if elem == 0 && i != 0 { return 0, 0, fmt.Errorf("alias") } if i == 2 && (elem&0x80) != 0 { return 0, 0, fmt.Errorf("byte three continues") } ln |= (elem & 0x7f) << (size * 7) size++ if (elem & 0x80) == 0 { break } } return size, uint16(ln), nil }