canopy-network/canopy · error

invalid tag at offset %d

Error message

invalid tag at offset %d

What it means

GetRawProtoField manually walks protobuf wire format. At each offset it decodes a field tag with protowire.ConsumeTag; a negative tag length means the bytes at that offset are not a valid varint tag — the buffer is corrupt, truncated, or not protobuf wire data at all.

Source

Thrown at lib/codec/codec.go:72

// ToAny() packs a protobuf message to a generic any
func (p *Protobuf) ToAny(message proto.Message) (*anypb.Any, error) {
	return anypb.New(message)
}

// FromAny() converts a proto any to the protobuf message
func (p *Protobuf) FromAny(any *anypb.Any) (proto.Message, error) {
	return anypb.UnmarshalNew(any, proto.UnmarshalOptions{})
}

// GetRawProtoField extracts the raw bytes for field number from a proto message
func GetRawProtoField(protoBytes []byte, fieldNumber int) ([]byte, error) {
	var offset int
	// parse the proto bytes to find a field
	for offset < len(protoBytes) {
		// decode the field tag (field number + wire type)
		fieldNum, wireType, tagLen := protowire.ConsumeTag(protoBytes[offset:])
		if tagLen < 0 {
			return nil, fmt.Errorf("invalid tag at offset %d", offset)
		}
		offset += tagLen
		// check if this is the field we're looking for
		if int(fieldNum) == fieldNumber {
			// for length-delimited fields (like messages), we need to read the length
			if wireType == protowire.BytesType {
				// read the length of the field value
				valueLen, lenBytes := protowire.ConsumeVarint(protoBytes[offset:])
				if lenBytes < 0 {
					return nil, fmt.Errorf("invalid length at offset %d", offset)
				}
				// calculate the new offset
				offset += lenBytes
				// extract the field value bytes
				if offset+int(valueLen) > len(protoBytes) {
					return nil, fmt.Errorf("field value exceeds buffer bounds")
				}
				// make buffer to return

View on GitHub (pinned to ee8197d91d)

Solutions

  1. Verify the bytes come from proto.Marshal (or codec.Marshal) of the expected message — log/inspect the first few bytes.
  2. If the data may be JSON or another format, branch on format before calling GetRawProtoField and decode accordingly.
  3. Round-trip sanity check: proto.Unmarshal(bytes, &msg{}) should succeed before doing raw wire scanning.

Example fix

// before
raw, _ := codec.GetRawProtoField(jsonBytes, 1)
// after
protoBytes, err := codec.Protobuf{}.Marshal(msg)
if err != nil { return err }
raw, err := codec.GetRawProtoField(protoBytes, 1)
Defensive patterns

Strategy: type-guard

Validate before calling

if len(data) == 0 {
    return fmt.Errorf("empty proto bytes")
}
var probe pb.Event
if err := proto.Unmarshal(data, &probe); err != nil {
    return fmt.Errorf("not valid protobuf wire data: %w", err)
}

Type guard

func isProtoWireData(data []byte) bool {
    var m ptypes.DynamicAny
    return proto.Unmarshal(data, &m.Message) == nil || protobufUnmarshalOK(data)
}

Try / catch

raw, err := codec.GetRawProtoField(data, fieldNum)
if err != nil && strings.Contains(err.Error(), "invalid tag") {
    return fmt.Errorf("corrupt or non-proto payload: %w", err)
}

Prevention

When it happens

Trigger: Calling GetRawProtoField (directly or via entryKeyOrZero / BytesToBlockHash) with bytes that are not valid proto wire format, or a truncated slice where the loop lands mid-tag.

Common situations: Passing JSON, hex/base64-decoded-but-wrong data, or a doubly/mis-encoded payload instead of proto.Marshal output; slicing the proto bytes at the wrong offset before calling; corrupted storage reads.

Understand the failure class

Background: "cannot parse invalid wire-format data", "cannot unmarshal", "failed unmarshalling": protobuf unmarshal errors explained — this error's family across 10 libraries.

Related errors


AI-assisted analysis of canopy-network/canopy@ee8197d91d (2026-09-06). Data as JSON: /api/errors/1dfbab09f97a65ee. Report an issue: GitHub.