Class: DEVp2p::Frame

Inherits:
Object
  • Object
show all
Extended by:
Configurable
Defined in:
lib/devp2p/frame.rb

Overview

When sending a packet over RLPx, the packet will be framed. The frame provides information about the size of the packet and the packet’s source protocol. There are three slightly different frames, depending on whether or not the frame is delivering a multi-frame packet. A multi-frame packet is a packet which is split (aka chunked) into multiple frames because it’s size is larger than the protocol window size (pws, see Multiplexing). When a packet is chunked into multiple frames, there is an implicit difference between the first frame and all subsequent frames.

Thus, the three frame types are normal, chunked-0 (first frame of a multi-frame packet), and chunked-n (subsequent frames of a multi-frame packet).

  • Single-frame packet:

    header || header-mac || frame || mac

  • Multi-frame packet:

    header || header-mac || frame-0 ||

    header || header-mac || frame-n || … ||

    header || header-mac || frame-last || mac

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Configurable

add_config

Constructor Details

#initialize(protocol_id, cmd_id, payload, sequence_id, window_size, is_chunked_n = false, frames = nil, frame_cipher = nil) ⇒ Frame

Returns a new instance of Frame.

Raises:

  • (ArgumentError)


51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/devp2p/frame.rb', line 51

def initialize(protocol_id, cmd_id, payload, sequence_id, window_size, is_chunked_n=false, frames=nil, frame_cipher=nil)
  raise ArgumentError, 'invalid protocol_id' unless protocol_id < TT16
  raise ArgumentError, 'invalid sequence_id' unless sequence_id.nil? || sequence_id < TT16
  raise ArgumentError, 'invalid window_size' unless window_size % padding == 0
  raise ArgumentError, 'invalid cmd_id' unless cmd_id < 256

  @protocol_id = protocol_id
  @cmd_id = cmd_id
  @payload = payload
  @sequence_id = sequence_id
  @is_chunked_n = is_chunked_n
  @frame_cipher = frame_cipher

  @frames = frames || []
  @frames.push self

  # chunk payloads resulting in frames exceeing window_size
  fs = frame_size
  if fs > window_size
    unless is_chunked_n
      @is_chunked_0 = true
      @total_payload_size = body_size
    end

    # chunk payload
    @payload = payload[0...(window_size-fs)]
    raise FrameError, "invalid frame size" unless frame_size <= window_size

    remain = payload[@payload.size..-1]
    raise FrameError, "invalid remain size" unless (remain.size + @payload.size) == payload.size

    Frame.new(protocol_id, cmd_id, remain, sequence_id, window_size, true, @frames, frame_cipher)
  end

  raise FrameError, "invalid frame size" unless frame_size <= window_size
end

Instance Attribute Details

#cmd_idObject (readonly)

Returns the value of attribute cmd_id.



49
50
51
# File 'lib/devp2p/frame.rb', line 49

def cmd_id
  @cmd_id
end

#framesObject (readonly)

Returns the value of attribute frames.



49
50
51
# File 'lib/devp2p/frame.rb', line 49

def frames
  @frames
end

#is_chunked_nObject (readonly)

Returns the value of attribute is_chunked_n.



49
50
51
# File 'lib/devp2p/frame.rb', line 49

def is_chunked_n
  @is_chunked_n
end

#payloadObject (readonly)

Returns the value of attribute payload.



49
50
51
# File 'lib/devp2p/frame.rb', line 49

def payload
  @payload
end

#protocol_idObject (readonly)

Returns the value of attribute protocol_id.



49
50
51
# File 'lib/devp2p/frame.rb', line 49

def protocol_id
  @protocol_id
end

#sequence_idObject (readonly)

Returns the value of attribute sequence_id.



49
50
51
# File 'lib/devp2p/frame.rb', line 49

def sequence_id
  @sequence_id
end

#total_payload_sizeObject (readonly)

Returns the value of attribute total_payload_size.



49
50
51
# File 'lib/devp2p/frame.rb', line 49

def total_payload_size
  @total_payload_size
end

Class Method Details

.decode_body_size(header) ⇒ Object



44
45
46
# File 'lib/devp2p/frame.rb', line 44

def decode_body_size(header)
  "\x00#{header[0,3]}".unpack('I>').first
end

.encode_body_size(size) ⇒ Object



40
41
42
# File 'lib/devp2p/frame.rb', line 40

def encode_body_size(size)
  [size].pack('I>')[1..-1]
end

Instance Method Details

#as_bytesObject

Raises:



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/devp2p/frame.rb', line 167

def as_bytes
  raise FrameError, 'can only be called once' if @cipher_called

  if @frame_cipher
    @cipher_called = true
    e = @frame_cipher.encrypt(header, body)
    raise FrameError, 'invalid frame size of encrypted frame' unless e.size == frame_size
    e
  else
    h = header
    raise FrameError, 'invalid header size' unless h.size == header_size

    b = body
    raise FrameError, 'invalid body size' unless b.size == body_size(true)

    dummy_mac = "\x00" * mac_size
    r = h + dummy_mac + b + dummy_mac
    raise FrameError, 'invalid frame' unless r.size == frame_size

    r
  end
end

#bodyObject

frame:

normal: rlp(packet_type) [|| rlp(packet_data)] || padding
chunked_0: rlp(packet_type) || rlp(packet_data ...)
chunked_n: rlp(...packet_data) || padding

padding: zero-fill to 16-byte boundary (only necessary for last frame)



163
164
165
# File 'lib/devp2p/frame.rb', line 163

def body
  Utils.rzpad16 "#{enc_cmd_id}#{payload}"
end

#body_size(padded = false) ⇒ Object

frame-size: 3-byte integer, size of frame, big endian encoded (excludes padding)



102
103
104
105
# File 'lib/devp2p/frame.rb', line 102

def body_size(padded=false)
  l = enc_cmd_id.size + payload.size
  padded ? Utils.ceil16(l) : l
end

#enc_cmd_idObject



152
153
154
# File 'lib/devp2p/frame.rb', line 152

def enc_cmd_id
  @is_chunked_n ? '' : RLP.encode(cmd_id, sedes: RLP::Sedes.big_endian_int)
end

#frame_sizeObject



93
94
95
96
# File 'lib/devp2p/frame.rb', line 93

def frame_size
  # header16 || mac16 || dataN + [padding] || mac16
  header_size + mac_size + body_size(true) + mac_size
end

#frame_typeObject



88
89
90
91
# File 'lib/devp2p/frame.rb', line 88

def frame_type
  return :normal if normal?
  @is_chunked_n ? :chunked_n : :chunked_0
end

#headerObject

header: frame-size || header-data || padding

frame-size: 3-byte integer, size of frame, big endian encoded header-data:

normal: RLP::Sedes::List.new(protocol_type[, sequence_id])
chunked_0: RLP::Sedes::List.new(protocol_type, sequence_id, total_packet_size)
chunked_n: RLP::Sedes::List.new(protocol_type, sequence_id)
normal, chunked_n: RLP::Sedes::List.new(protocol_type[, sequence_id])
values:
  protocol_type: < 2**16
  sequence_id: < 2**16 (this value is optional for normal frames)
  total_packet_size: < 2**32

padding: zero-fill to 16-byte boundary

Raises:



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/devp2p/frame.rb', line 126

def header
  raise FrameError, "invalid protocol id" unless protocol_id < 2**16
  raise FrameError, "invalid sequence id" unless sequence_id.nil? || sequence_id < TT16

  l = [protocol_id]
  if @is_chunked_0
    raise FrameError, 'chunked_0 must have sequence_id' if sequence_id.nil?
    l.push sequence_id
    l.push total_payload_size
  elsif sequence_id
    l.push sequence_id
  end

  header_data = RLP.encode l, sedes: header_sedes
  raise FrameError, 'invalid rlp' unless l == RLP.decode(header_data, sedes: header_sedes, strict: false)

  bs = body_size
  raise FrameError, 'invalid body size' unless bs < 256**3

  header = Frame.encode_body_size(body_size) + header_data
  header = Utils.rzpad16 header
  raise FrameError, 'invalid header' unless header.size == header_size

  header
end

#normal?Boolean

Returns:

  • (Boolean)


107
108
109
# File 'lib/devp2p/frame.rb', line 107

def normal?
  !@is_chunked_n && !@is_chunked_0
end

#to_sObject Also known as: inspect



190
191
192
# File 'lib/devp2p/frame.rb', line 190

def to_s
  "<Frame(#{frame_type}, len=#{frame_size}, protocol=#{protocol_id} sid=#{sequence_id})"
end