Repository: scionproto/scion
Pull request #3812 (2020-06-30 14:18:36)

combinator: enable header v2 path combination
Add combinator logic that is capable of handling v2 segments.

The combinator decides whether a segment is v1 or v2 based on the
value in the ISD field of the control plane segment. In v1, this
field is always non-zero, in header v2 this is always zero.

The SCION Daemon API is extended such that it attaches the header version
to the replies. Client logic then decides whether to initialize the
path as header v1 or header v2 path.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/scionproto/scion/3812)
<!-- Reviewable:end -->



Patch:
diff --git a/go/cs/segutil/router.go b/go/cs/segutil/router.go
index 060dd3147..9328bf9ab 100644
--- a/go/cs/segutil/router.go
+++ b/go/cs/segutil/router.go
@@ -77,9 +77,14 @@ func (r *Router) translate(comb *combinator.Path, dst addr.IA) (path, error) {
 	if _, err := comb.WriteTo(buf); err != nil {
 		return path{}, err
 	}
-	sp := spath.New(buf.Bytes())
-	if err := sp.InitOffsets(); err != nil {
-		return path{}, err
+	var sp *spath.Path
+	if !comb.HeaderV2 {
+		sp = spath.New(buf.Bytes())
+		if err := sp.InitOffsets(); err != nil {
+			return path{}, err
+		}
+	} else {
+		sp = spath.NewV2(buf.Bytes())
 	}
 	nextHop, ok := r.Pather.TopoProvider.Get().UnderlayNextHop(comb.Interfaces[0].IfID)
 	if !ok {
diff --git a/go/lib/ctrl/seg/signed.go b/go/lib/ctrl/seg/signed.go
index f86cb4732..5d573db59 100644
--- a/go/lib/ctrl/seg/signed.go
+++ b/go/lib/ctrl/seg/signed.go
@@ -18,6 +18,7 @@ package seg
 import (
 	"fmt"
 
+	"github.com/scionproto/scion/go/lib/addr"
 	"github.com/scionproto/scion/go/lib/common"
 	"github.com/scionproto/scion/go/lib/spath"
 	"github.com/scionproto/scion/go/proto"
@@ -31,6 +32,7 @@ type PathSegmentSignedData struct {
 
 	// TODO(roosd): Remove when completely switching to v2.
 	RawInfo common.RawBytes `capnp:"infoF"`
+	ISD     addr.ISD        `capnp:"-"`
 }
 
 func NewPathSegmentSignedDataFromRaw(b []byte) (*PathSegmentSignedData, error) {
@@ -46,6 +48,7 @@ func NewPathSegmentSignedDataFromRaw(b []byte) (*PathSegmentSignedData, error) {
 		return nil, err
 	}
 	pss.RawTimestamp = info.TsInt
+	pss.ISD = addr.ISD(info.ISD)
 	return pss, nil
 }
 
diff --git a/go/lib/infra/modules/combinator/BUILD.bazel b/go/lib/infra/modules/combinator/BUILD.bazel
index a0239b55c..7df4ae019 100644
--- a/go/lib/infra/modules/combinator/BUILD.bazel
+++ b/go/lib/infra/modules/combinator/BUILD.bazel
@@ -14,7 +14,11 @@ go_library(
         "//go/lib/common:go_default_library",
         "//go/lib/ctrl/seg:go_default_library",
         "//go/lib/sciond:go_default_library",
+        "//go/lib/serrors:go_default_library",
+        "//go/lib/slayers/path:go_default_library",
+        "//go/lib/slayers/path/scion:go_default_library",
         "//go/lib/spath:go_default_library",
+        "//go/lib/util:go_default_library",
         "//go/proto:go_default_library",
     ],
 )
@@ -31,7 +35,7 @@ go_test(
         "//go/lib/addr:go_default_library",
         "//go/lib/common:go_default_library",
         "//go/lib/ctrl/seg:go_default_library",
-        "//go/lib/spath:go_default_library",
+        "//go/lib/util:go_default_library",
         "//go/lib/xtest:go_default_library",
         "//go/lib/xtest/graph:go_default_library",
         "@com_github_golang_mock//gomock:go_default_library",
diff --git a/go/lib/infra/modules/combinator/combinator.go b/go/lib/infra/modules/combinator/combinator.go
index fb994081e..04decdd2c 100644
--- a/go/lib/infra/modules/combinator/combinator.go
+++ b/go/lib/infra/modules/combinator/combinator.go
@@ -33,7 +33,11 @@ import (
 	"github.com/scionproto/scion/go/lib/common"
 	"github.com/scionproto/scion/go/lib/ctrl/seg"
 	"github.com/scionproto/scion/go/lib/sciond"
+	"github.com/scionproto/scion/go/lib/serrors"
+	"github.com/scionproto/scion/go/lib/slayers/path"
+	"github.com/scionproto/scion/go/lib/slayers/path/scion"
 	"github.com/scionproto/scion/go/lib/spath"
+	"github.com/scionproto/scion/go/lib/util"
 	"github.com/scionproto/scion/go/proto"
 )
 
@@ -73,6 +77,8 @@ type Path struct {
 	Mtu        uint16
 	Interfaces []sciond.PathInterface
 	StaticInfo *PathMetadata
+
+	HeaderV2 bool
 }
 
 func (p *Path) writeTestString(w io.Writer) {
@@ -116,14 +122,78 @@ func (p *Path) ComputeExpTime() time.Time {
 }
 
 func (p *Path) WriteTo(w io.Writer) (int64, error) {
+	if !p.HeaderV2 {
+		return p.writeLegacy(w)
+	}
+	var meta scion.MetaHdr
+	var infos []*path.InfoField
+	var hops []*path.HopField
+
+	for i, segment := range p.Segments {
+		meta.SegLen[i] = uint8(len(segment.HopFields))
+		infos = append(infos, &path.InfoField{
+			ConsDir:   segment.InfoField.ConsDir,
+			Peer:      segment.InfoField.Peer,
+			SegID:     segment.InfoField.SegID,
+			Timestamp: util.TimeToSecs(segment.InfoField.Timestamp),
+		})
+		for _, hop := range segment.HopFields {
+			hops = append(hops, &path.HopField{
+				ExpTime:     hop.ExpTime,
+				ConsIngress: hop.ConsIngress,
+				ConsEgress:  hop.ConsEgress,
+				Mac:         hop.MAC,
+			})
+		}
+	}
+	sp := scion.Decoded{
+		Base: scion.Base{
+			PathMeta: meta,
+			NumHops:  len(hops),
+			NumINF:   len(p.Segments),
+		},
+		InfoFields: infos,
+		HopFields:  hops,
+	}
+	raw := make([]byte, sp.Len())
+	if err := sp.SerializeTo(raw); err != nil {
+		return 0, err
+	}
+	n, err := w.Write(raw)
+	if err != nil {
+		return int64(n), err
+	}
+	if n != len(raw) {
+		return int64(n), serrors.New("incomplete path written", "expected", len(raw), "actual", n)
+	}
+	return int64(n), nil
+}
+
+func (p *Path) writeLegacy(w io.Writer) (int64, error) {
 	var total int64
 	for _, segment := range p.Segments {
-		n, err := segment.InfoField.WriteTo(w)
+		info := spath.InfoField{
+			ConsDir:  segment.InfoField.ConsDir,
+			Shortcut: segment.InfoField.Shortcut,
+			Peer:     segment.InfoField.Peer,
+			Hops:     uint8(segment.InfoField.Hops),
+			ISD:      uint16(segment.InfoField.ISD),
+			TsInt:    util.TimeToSecs(segment.InfoField.Timestamp),
+		}
+		n, err := info.WriteTo(w)
 		total += n
 		if err != nil {
 			return total, err
 		}
 		for _, hopField := range segment.HopFields {
+			hopField := spath.HopField{
+				VerifyOnly:  hopField.VerifyOnly,
+				Xover:       hopField.Xover,
+				ExpTime:     spath.ExpTimeType(hopField.ExpTime),
+				ConsIngress: common.IFIDType(hopField.ConsIngress),
+				ConsEgress:  common.IFIDType(hopField.ConsEgress),
+				Mac:         hopField.MAC,
+			}
 			n, err := hopField.WriteTo(w)
 			total += n
 			if err != nil {
@@ -144,30 +214,26 @@ type Segment struct {
 // initInfoFieldFrom copies the info field in pathSegment, and sets it as the
 // info field of segment.
 func (segment *Segment) initInfoFieldFrom(pathSegment *seg.PathSegment) {
-	infoField, err := spath.InfoFFromRaw(pathSegment.SData.RawInfo)
-	if err != nil {
-		panic(err)
-	}
 	segment.InfoField = &InfoField{
-		InfoField: infoField,
+		ISD:       pathSegment.SData.ISD,
+		SegID:     pathSegment.SData.SegID,
+		Timestamp: pathSegment.Timestamp(),
 	}
 }
 
 // appendHopFieldFrom copies the Hop Field in entry, and appends it to segment.
 func (segment *Segment) appendHopFieldFrom(entry *seg.HopEntry) *HopField {
 	hopField := &HopField{
-		HopField: &spath.HopField{
-			ExpTime:     spath.ExpTimeType(entry.HopField.ExpTime),
-			ConsIngress: common.IFIDType(entry.HopField.ConsIngress),
-			ConsEgress:  common.IFIDType(entry.HopField.ConsEgress),
-			Mac:         append([]byte(nil), entry.HopField.MAC...),
-		},
+		ExpTime:     entry.HopField.ExpTime,
+		ConsIngress: entry.HopField.ConsIngress,
+		ConsEgress:  entry.HopField.ConsEgress,
+		MAC:         append([]byte(nil), entry.HopField.MAC...),
 	}
 	segment.HopFields = append(segment.HopFields, hopField)
 	if segment.InfoField.Hops == 0xff {
 		panic("too many hops")
 	}
-	segment.InfoField.Hops += 1
+	segment.InfoField.Hops++
 	return hopField
 }
 
@@ -181,13 +247,13 @@ func (segment *Segment) reverse() {
 }
 
 func (segment *Segment) ComputeExpTime() time.Time {
-	return segment.InfoField.Timestamp().Add(segment.computeHopFieldsTTL())
+	return segment.InfoField.Timestamp.Add(segment.computeHopFieldsTTL())
 }
 
 func (segment *Segment) computeHopFieldsTTL() time.Duration {
 	minTTL := time.Duration(spath.MaxTTL) * time.Second
 	for _, hf := range segment.HopFields {
-		offset := hf.ExpTime.ToDuration()
+		offset := spath.ExpTimeType(hf.ExpTime).ToDuration()
 		if minTTL > offset {
 			minTTL = offset
 		}
@@ -196,7 +262,15 @@ func (segment *Segment) computeHopFieldsTTL() time.Duration {
 }
 
 type InfoField struct {
-	*spath.InfoField
+	SegID     uint16
+	Timestamp time.Time
+	Peer      bool
+	ConsDir   bool
+
+	// legacy
+	Shortcut bool
+	ISD      addr.ISD
+	Hops     int // Not updated in v2 path. Use len()
 }
 
 func (field *InfoField) String() string {
@@ -208,7 +282,14 @@ func (field *InfoField) String() string {
 }
 
 type HopField struct {
-	*spath.HopField
+	ExpTime     uint8
+	ConsIngress uint16
+	ConsEgress  uint16
+	MAC         []byte
+
+	// legacy
+	Xover      bool
+	VerifyOnly bool
 }
 
 func (field *HopField) String() string {
diff --git a/go/lib/infra/modules/combinator/graph.go b/go/lib/infra/modules/combinator/graph.go
index 512fa90ed..eb43b243f 100644
--- a/go/lib/infra/modules/combinator/graph.go
+++ b/go/lib/infra/modules/combinator/graph.go
@@ -16,6 +16,7 @@ package combinator
 
 import (
 	"bytes"
+	"encoding/binary"
 	"fmt"
 	"sort"
 
@@ -270,6 +271,104 @@ type PathSolution struct {
 // GetFwdPathMetadata builds the complete metadata for a forwarding path by
 // extracting it from a path between source and destination in the DMG.
 func (solution *PathSolution) GetFwdPathMetadata() *Path {
+	var legacy bool
+	for _, edge := range solution.edges {
+		if edge.segment.PathSegment.SData.ISD != 0 {
+			legacy = true
+		} else if legacy {
+			panic("mixed segment types")
+		}
+	}
+	if legacy {
+		return solution.legacyFwdPathMetadata()
+	}
+	path := &Path{
+		Weight:   solution.cost,
+		Mtu:      ^uint16(0),
+		HeaderV2: true,
+	}
+	for _, solEdge := range solution.edges {
+		var hops []*HopField
+		var intfs []sciond.PathInterface
+
+		// Go through each ASEntry, starting from the last one, until we
+		// find a shortcut (which can be 0, meaning the end of the segment).
+		asEntries := solEdge.segment.ASEntries
+		for asEntryIdx := len(asEntries) - 1; asEntryIdx >= solEdge.edge.Shortcut; asEntryIdx-- {
+			isShortcut := asEntryIdx == solEdge.edge.Shortcut && solEdge.edge.Shortcut != 0
+			isPeer := asEntryIdx == solEdge.edge.Shortcut && solEdge.edge.Peer != 0
+			asEntry := asEntries[asEntryIdx]
+
+			// Regular hop field.
+			entry := asEntry.HopEntries[0]
+			hopField := &HopField{
+				ExpTime:     entry.HopField.ExpTime,
+				ConsIngress: entry.HopField.ConsIngress,
+				ConsEgress:  entry.HopField.ConsEgress,
+				MAC:         append([]byte(nil), entry.HopField.MAC...),
+			}
+
+			// We don't know if this hop entry is used for forwarding (a peer
+			// entry might be used instead, which would override mtu later)
+			forwardingLinkMtu := entry.InMTU
+
+			if isPeer {
+				// We've reached the ASEntry where we want to switch
+				// segments on a peering link.
+				peer := asEntry.HopEntries[solEdge.edge.Peer]
+				hopField = &HopField{
+					ExpTime:     peer.HopField.ExpTime,
+					ConsIngress: peer.HopField.ConsIngress,
+					ConsEgress:  peer.HopField.ConsEgress,
+					MAC:         append([]byte(nil), peer.HopField.MAC...),
+				}
+				forwardingLinkMtu = peer.InMTU
+			}
+
+			// Segment is traversed in reverse construction direction.
+			// Only include non-zero interfaces.
+			if hopField.ConsEgress != 0 {
+				intfs = append(intfs, sciond.PathInterface{
+					RawIsdas: asEntry.RawIA,
+					IfID:     common.IFIDType(hopField.ConsEgress),
+				})
+			}
+			// In a non-peer shortcut the AS is not traversed completely.
+			if hopField.ConsIngress != 0 && (!isShortcut || isPeer) {
+				intfs = append(intfs, sciond.PathInterface{
+					RawIsdas: asEntry.RawIA,
+					IfID:     common.IFIDType(hopField.ConsIngress),
+				})
+			}
+			hops = append(hops, hopField)
+
+			path.Mtu = minUint16(path.Mtu, asEntry.MTU)
+			if forwardingLinkMtu != 0 {
+				// The first HE in a segment has MTU 0, so we ignore those
+				path.Mtu = minUint16(path.Mtu, forwardingLinkMtu)
+			}
+		}
+		path.Segments = append(path.Segments, &Segment{
+			Type: solEdge.segment.Type,
+			InfoField: &InfoField{
+				Timestamp: solEdge.segment.Timestamp(),
+				SegID:     calculateBeta(solEdge),
+				ConsDir:   solEdge.segment.IsDownSeg(),
+				Peer:      solEdge.edge.Peer != 0,
+				Hops:      len(hops),
+			},
+			HopFields:  hops,
+			Interfaces: intfs,
+		})
+	}
+	path.reverseDownSegment()
+	path.aggregateInterfaces()
+	path.StaticInfo = solution.collectMetadata()
+	return path
+
+}
+
+func (solution *PathSolution) legacyFwdPathMetadata() *Path {
 	path := &Path{
 		Weight: solution.cost,
 		Mtu:    ^uint16(0),
@@ -297,7 +396,7 @@ func (solution *PathSolution) GetFwdPathMetadata() *Path {
 			// We don't know if this hop entry is used for forwarding (a peer
 			// entry might be used instead, which would override mtu later)
 			forwardingLinkMtu := hopEntry.InMTU
-			inIFID, outIFID = newHF.ConsEgress, newHF.ConsIngress
+			inIFID, outIFID = common.IFIDType(newHF.ConsEgress), common.IFIDType(newHF.ConsIngress)
 
 			// If we've transitioned from a previous segment, set Xover flag.
 			if edgeIdx > 0 {
@@ -336,7 +435,8 @@ func (solution *PathSolution) GetFwdPathMetadata() *Path {
 						pHF := currentSeg.appendHopFieldFrom(pHopEntry)
 						forwardingLinkMtu = pHopEntry.InMTU
 						pHF.Xover = true
-						inIFID, outIFID = pHF.ConsEgress, pHF.ConsIngress
+						inIFID, outIFID = common.IFIDType(pHF.ConsEgress),
+							common.IFIDType(pHF.ConsIngress)
 					} else {
 						// Normal shortcut, so only half of this HF is traversed by the packet
 						outIFID = 0
@@ -362,6 +462,24 @@ func (solution *PathSolution) GetFwdPathMetadata() *Path {
 	return path
 }
 
+func calculateBeta(edge *solutionEdge) uint16 {
+	var index int
+	if edge.segment.IsDownSeg() {
+		index = edge.edge.Shortcut
+		if edge.edge.Peer != 0 {
+			index++
+		}
+	} else {
+		index = len(edge.segment.ASEntries)
+	}
+	beta := edge.segment.SData.SegID
+	for i := 0; i < index; i++ {
+		hop := edge.segment.ASEntries[i].HopEntries[0]
+		beta = beta ^ binary.BigEndian.Uint16(hop.HopField.MAC)
+	}
+	return beta
+}
+
 // PathSolutionList is a sort.Interface implementation for a slice of solutions.
 type PathSolutionList []*PathSolution
 
diff --git a/go/lib/sciond/apitypes.go b/go/lib/sciond/apitypes.go
index e616f5f93..db2439cde 100644
--- a/go/lib/sciond/apitypes.go
+++ b/go/lib/sciond/apitypes.go
@@ -133,10 +133,17 @@ func pathReplyEntryToPath(pe PathReplyEntry, dst addr.IA) (Path, error) {
 			expiry: pe.Path.Expiry(),
 		}, nil
 	}
-	sp := spath.New(pe.Path.FwdPath)
-	if err := sp.InitOffsets(); err != nil {
-		return Path{}, serrors.WrapStr("path error", err)
+
+	var sp *spath.Path
+	if !pe.Path.HeaderV2 {
+		sp = spath.New(pe.Path.FwdPath)
+		if err := sp.InitOffsets(); err != nil {
+			return Path{}, serrors.WrapStr("path error", err)
+		}
+	} else {
+		sp = spath.NewV2(pe.Path.FwdPath)
 	}
+
 	underlayAddr := pe.HostInfo.Underlay()
 	p := Path{
 		interfaces: make([]pathInterface, 0, len(pe.Path.Interfaces)),
diff --git a/go/lib/sciond/types.go b/go/lib/sciond/types.go
index 11acccef2..959285bfb 100644
--- a/go/lib/sciond/types.go
+++ b/go/lib/sciond/types.go
@@ -188,6 +188,8 @@ type FwdPathMeta struct {
 	Mtu        uint16
 	Interfaces []PathInterface
 	ExpTime    uint32
+
+	HeaderV2 bool
 }
 
 func (fpm *FwdPathMeta) SrcIA() addr.IA {
@@ -214,7 +216,7 @@ func (fpm *FwdPathMeta) Copy() *FwdPathMeta {
 	if fpm == nil {
 		return nil
 	}
-	res := &FwdPathMeta{Mtu: fpm.Mtu, ExpTime: fpm.ExpTime}
+	res := &FwdPathMeta{Mtu: fpm.Mtu, ExpTime: fpm.ExpTime, HeaderV2: fpm.HeaderV2}
 	res.FwdPath = common.CloneByteSlice(fpm.FwdPath)
 	if fpm.Interfaces != nil {
 		res.Interfaces = make([]PathInterface, len(fpm.Interfaces))
diff --git a/go/lib/spath/path.go b/go/lib/spath/path.go
index 296bcc961..f51911074 100644
--- a/go/lib/spath/path.go
+++ b/go/lib/spath/path.go
@@ -171,6 +171,9 @@ func (p *Path) Reverse() error {
 // InitOffsets computes the initial Hop Field offset (in bytes) for a newly
 // created packet.
 func (path *Path) InitOffsets() error {
+	if path.version == 2 {
+		return nil
+	}
 	var err error
 	var infoF *InfoField
 	var hopF *HopField
@@ -205,6 +208,9 @@ func (path *Path) InitOffsets() error {
 // IncOffsets updates the info and hop indices to the next routing field, while skipping
 // verify only fields.
 func (path *Path) IncOffsets() error {
+	if path.version == 2 {
+		panic("not supported")
+	}
 	var err error
 	if path.HopOff == 0 {
 		// Path not initialized yet
diff --git a/go/pkg/sciond/fetcher/fetcher.go b/go/pkg/sciond/fetcher/fetcher.go
index 2f916857c..90ba6f51b 100644
--- a/go/pkg/sciond/fetcher/fetcher.go
+++ b/go/pkg/sciond/fetcher/fetcher.go
@@ -177,6 +177,7 @@ func (f *fetcher) translate(path *combinator.Path) (sciond.PathReplyEntry, error
 			Mtu:        path.Mtu,
 			Interfaces: path.Interfaces,
 			ExpTime:    uint32(path.ComputeExpTime().Unix()),
+			HeaderV2:   path.HeaderV2,
 		},
 		HostInfo: hostinfo.FromUDPAddr(*nextHop),
 	}
diff --git a/go/proto/sciond.capnp.go b/go/proto/sciond.capnp.go
index 4ac40c150..dc96ded0f 100644
--- a/go/proto/sciond.capnp.go
+++ b/go/proto/sciond.capnp.go
@@ -1604,6 +1604,14 @@ func (s FwdPathMeta) SetExpTime(v uint32) {
 	s.Struct.SetUint32(4, v)
 }
 
+func (s FwdPathMeta) HeaderV2() bool {
+	return s.Struct.Bit(16)
+}
+
+func (s FwdPathMeta) SetHeaderV2(v bool) {
+	s.Struct.SetBit(16, v)
+}
+
 // FwdPathMeta_List is a list of FwdPathMeta.
 type FwdPathMeta_List struct{ capnp.List }
 
@@ -2779,166 +2787,167 @@ func (p SegTypeHopReplyEntry_Promise) Struct() (SegTypeHopReplyEntry, error) {
 	return SegTypeHopReplyEntry{s}, err
 }
 
-const schema_8f4bd412642c9517 = "x\xda\x94X}l\x1c\xe5\xd1\x9fy\xf6|\xe78>" +
-	"\xdfmv\x0d\x91_\xbd\\\x9b\x82\xf2!ba\x87\x14" +
-	"\x14\x01\x8e\x9d\xcfKm\xf0s\x97\xb4\x80\x12\x95\x8d\xef" +
-	"\xb1\xbdp\xbe\xbb\xdc\xae\x9d\x18A\xdd \xd2\xd2\xb4\x15" +
-	"P\x88\x0a\x85\xa8\x09\x10\x88\xdbD%\xd4|\xa5-*" +
-	"\x82\xb4%J\xf9\xa8\xa8P#\x0a!%\x90\x00\x95H" +
-	"\x08\x0dNC\xb7\x9a\xdd\xbd\xdd\xcd\xfa\x1c\xe8\x7f{;" +
-	"\xb3\xf3\xcc\xfcf\xe67\xf3\xdc%\xf7D\x17\xb2\x96\x9a" +
-	"\xc7\xa6\x00\xf0RM\xd4\xfa\xe4\xf1\xdd\x8f~x\xf2\xe6" +
-	"\xef\x83\x1cG\xeb\xfc-\x17\xe7\xa6\xbd\xfe\x8d;\xa1\x06" +
-	"c\x00\xca\xb3\x91\x83\xca\xbe\x08==\x1fi\x03\xb4\xe6" +
-	"\xbeu\xe5\xce\x91\x99cw\x00oB\xb4\x8e\xcc_\xb4" +
-	"\xa2\xe5\xd4+;\\\xe5\xe3\x91\xd3\xca\xe7\xb6\xf2xd" +
-	"=\xa0u\xf2\xe0\xf8\xb7\x9f\xdb\xff\xf6f\xe0q\x0cZ" +
-	"f\xa4\xb2\xaaf\xbf\xa2\xd5\xd0\xd3\x9a\x9a\xf7\x01\xad&" +
-	"\xf9\xc1\xa5\xef\x967\xde\x19R\xb6\xcd-\x89>\xa1t" +
-	"E\xe9)\x1d%/\x96\xbe\xb0td\xec\x81\x0f\xee&" +
-	"]\xe6\xeb.a\xb18F\x14=\xbaWYG\xda\xf3" +
-	"\x06\xa2\xd7H\x80\xd6\xd6\xa3\xea\xe1Y\xd3\xbfso\xb5" +
-	"\x00\xa7\xd4\xedW\x1a\xeb\xe8I\xae#\xd3\xdbo\x9d\xba" +
-	"s\xfe\xc2\xe1-!\xd3\xb6\x1b\xedu\x07\x95.[7" +
-	"]G\xf1\x1d\xebx{\xd3c\x9b\xa2\x0fT\xb3\xbb\xa3" +
-	"\xee\x03e\x8f\xad\xbb\xdb\xb6{\xf0o\x9b\x8f\xbeS\xf3" +
-	"\xf2\x03\xc0\x1bQ\xb2>|\xf8\xc57[\x1a\xff\xf0\"" +
-	"4b\x0c\x01\x94\xd7\xea\x0e\x02*\x7f\xb5\xadz\xa0\x86" +
-	"P\x93\xc8X\xcb\xd4\xbf(WN\xbd\x0c`\x9e\x98\xfa" +
-	"-\x04\xb4\xa6\xb5lkY]{\xcdh\x15\x1f\xe6\x8d" +
-	"\xd73Tj\xe2\xf4\x1d\xc6\xc9\x89\xb1\xe3\xa3\xfc\xfa\xe9" +
-	"\x9f\xed\x0a\x9b\xb6\xb5\xe7\xc7\xa7\xa1\xb2\xc4\xd6n\x8f\xff" +
-	"\x0a\xd0\xfa\xcaE\xf7\xac\xaf\x99\xd9\xf4D\xd5\xf4\x1d\x8f" +
-	"?\xa1\x8c\xdb\xca\x9f\xc6\xc9k\xeb\x95\x03\xd7\x1e\xdbr" +
-	"\xcbX\xd5\xc2\x98\xdfpBio8\x9f\x80k \xe5" +
-	"\xd6wk\xbff\xbc\xf5\xf2\xd3\xc0/\xc0\xc0\xa7\x8e\x1f" +
-	"\xcf6\xb4\xa2\xf2R\x03}\xb7\xaf\x81\xfc8z\xe2\xbc" +
-	"\xa1#\xff\\\xf8B5\x98E\xe2\x03e]\x82\x9e\x06" +
-	"\x12\x14\xa1\x07,\x8f\xa3\x14V\xde\x9a\xf8\x85\xb2\x83\x94" +
-	"\xe7mO\xa4\x08\xbc\x8f\x86~ZZ\xd9l\xed\x0bY" +
-	"\xb6\x91\x1eM\x1eV\x9eL\xd2\xd3\x9e$y\x91\x10\xaf" +
-	"\xb4w\xdc~\xc1\xfej\xf5\xc9\xe5\x83\xca\x1a\x99\x9e\xae" +
-	"\x93\xc9\x8b\x1d\xef]\xf8\xe0\xce\x87\xc4\x81j\xba\xc3\xf2" +
-	"^e\xa3\xad{\xab\xad\xbbF^\xfd\xf4\xd0\xac\x97\xde" +
-	" \xe0\x98\x0f\x1c\xe9\xce\xdb*\xafEe\x8f\xad\xbc[" +
-	"&'\xde|\xe77\x8f\xfe\xe0\x9e\x99\xefWM`\xd7" +
-	"\xb4&T\xd6L\xb3\xdd\x98F\xda\xf9C\x99o6\xbd" +
-	"v\xea\xfd\xaau\xaf\xecW\x1a\x15\xbb\xee\x15r\xe3\xf2" +
-	"\x99o|\xaf\xafq\xdf\xc7\xd5,+\xed\xca\x09\xa5\xcb" +
-	"VN+\x94\xbf\xb6\xf7\xae\x9a\xfd\xd4\xb1\xc4\xf1\xaa\xca" +
-	"\xa3\xca^e\x8f\xad\xbc\xdbV~\xf6\xb9\x0d\xa3?|" +
-	"\xe3\xd1S\xd5\xbc\xf8\x7f\xf5\x842[\xa5\xa7\x8bT\xf2" +
-	"\xa2\xbe\xe9\xad_\xf6]td\x1c\xf8y\x18(\xc0F" +
-	"fw\xc9*\xf50\xa0r\x9dJV\x7f\xfd\xd4\xcd\xcb" +
-	"\xc6\x1e\xdes\xbaZ\x9f>\xa9\x9eP\x9e\xb7\xad\xfeN" +
-	"%\x1c\x8c\x1e\xbdX\xc85\xf70\xadT(-H/" +
-	"M\x17z\x8b\x19\xb1nPH\x86\xd9\x8d\xc8#R\x04" +
-	" \x82\x00r\xbc\x15\x80\xd7J\xc8/d\x98\xd2{\xd3" +
-	"\x8b\x0dl\x00\xec\x96\x10\xa7\x00\xa3\xc7\x90\xadn\xcd\xec" +
-	"\xef\x12\xa6\x96\xd3\xcc6\xad\xf9\xea\xa2)\xc8^\xadg" +
-	"o\xf6\x1c\x00~\xa1\x84\xfc\x12\x86\x88*\xd2\xbb\xb9t" +
-	"\xc6,\x09\xf9\xa5\x0c\x13\x85\xa2)\xb0\x1e\x18\xd6\x03\xa6" +
-	"t#\xa7\x19\xf6IS&\x9c\xb4t}\xaer\x18\x00" +
-	"\x1d\x92\xf4\x0e\xd1:\x00\xf8j\x09y\xbf\x7f\x88\x98\x01" +
-	"\xc0o\x90\x90\xe7\x19\xca\x0cUd\x00\xb2~=\x00\xef" +
-	"\x97\x90\xdf\xceP\x96PE\x09@\xdeH_\xdf\"!" +
-	"\xbf\x83\xe1H\xafs\x0a\xc6\x81a\x1c06`\x0eb" +
-	"\x0c\x18\xc6\x00-\xbd`\x8ar\xaf\xd6\x03\x92\xf0PI" +
-	"\xfa|\x09H/G\xc4\x86\xd2J}@`-0\xac" +
-	"\x0dD\x81v\x14\x191\x94\xca\x88R~8\x04\xfb\x02" +
-	"\x17v\x95a[Y\x18\x83y\xd3;\xf6l\x03\xd9E" +
-	"\xe9\xb6k\xae^\xdce\xf4\x91\x85+*\x16\x94-\xd8" +
-	"\x04\x90\xbd\x0b%\xcc>\x88\x0c\xe3hY6\x10\xca\xfd" +
-	"\xd8\x0a\x90\xbd\x97\x04\xdbH\xc0\xfec\xd9`([\xb1" +
-	"\x03 {\x1f\x09\x1e!\x81\xf4\xb9e\x03\xa2l\xc7\x0c" +
-	"@v\x1b\x09v\x91 r\xc6R1B\x85m\x0bv" +
-	"\x92`\x8c\x045\xff\xb6T\xac!\x82\xc0\xb5\x00\xd9\xc7" +
-	"I\xf0[\x12DO[*Fiz\xe2m\x00\xd9g" +
-	"H\xf0\"\x09b\xe3\x96j\xd7\xfd\xf3X\x06\xc8\xfe\x9e" +
-	"\x04\x07HP\xfb\x99\xa5b-\x80\xf2\x92m\xeaO$" +
-	"x\x9d\x04SNY*N\xa1\x09\x81?\x03\xc8\xbeN" +
-	"\x82C$\xa8\xfb\x97\xa5b\x1d\x80\xf2w\xdc\x0c\x90=" +
-	"D\x82\x8fH0\xf5SK\xc5\xa9\x00\xca1\\\x01\x90" +
-	"=J\x82\x93$\xa8?i\xa9XO\xd4m\x1f\xfe1" +
-	"\x09\xce\x90 \xfe\x89\xa5b\x9c\xa6\xb6\xed\xeeg$\x88" +
-	"0\x86r\x03\xaa\xd8@\xc3\x83\x11Tg\xe8}-c" +
-	"(\xe9\xb9J\x91\xa6\x06\x0b\x860!:R\xd2\xcc\xfe" +
-	"\x8cX\x87I\x9f\x81\x011\x09h9\x92R\x1ep\x18" +
-	"\x93>s\xb8R\xcdp\x9a\x11\x90\xbe\xf5x3,\x8d" +
-	"\x95\xf2\xf4\xb57\xc8]yY\x0c]]4\xf5^\xd4" +
-	"{4S/\x16\x00\x93\xfePvu\xf4^\xd7Fj" +
-	"\xdd\xa00LL\xfa\xfbNX\xc3=\xc5\xa3MWn" +
-	"\x88\xf2\x90\xde#\xd2\x18\xa0\x0dL\xfa\x93\xb7\xaaZ)" +
-	"?\x0c\xe4\x8e\xc7~\xbe\xcb\xae\x90\xa4\xde\xd2\xe3\xd9\xe8" +
-	"[9\\\x12\xcb!U,9pzc'\xa4\x81\xa4" +
-	"@v0\xe9\x0fHGg\xc4,k=\"\x9d\xabt" +
-	"q\x88I\xda\xb3i\xdf\xc3P\x1fv\xf8\xf47\"\x0a" +
-	"fY\x0f\xb6\xbaG\xb9N\xabW\xa1\xc2\xb4C\x11R" +
-	"O\x98\x06[}\x1a\x94=\x1e\x9c\xe3\xf3\xe0\xd9\xd4\x97" +
-	" \xe6\x9d\x84\x073n\xca\xf5\x1e-A)\x0f\x05\xb0" +
-	"\x02\x80\xd7K\xc8\xa73\xb4\x8c\x8c\x18\xa2P\x1d\xa83" +
-	"\xff8}\xd9\xa6e\xad?\xf7\x80\x9c\xe0}F\xack" +
-	"\xee\xcdkR\x9fA\xae'\xefr\xf8qvG\xd0\xf7" +
-	"\xbbm*\x90\xe7.\xf0}\x1f)\x8b\xde\xb20\xfa\x11" +
-	"\x81!\x02\xb6\xf5\xeb\xb9\x9c(T~N>1P\xe3" +
-	"\xf5\x18X\x00d\xf9'\xfe.$7\xce\xf0\x97mY" +
-	"\x9ec\xb9\xd8\xf6\xa0\xe8\xd4\x0b7\xad\x1c.\xa1\x88-" +
-	"\x13\xc5\x04\x0d\x1d>\xddC\xe0\xfe\x1b\x01\xf8}\x12\xf2" +
-	"G\xc8]\xe6@\xbd=\x03\xc0\xb7I\xc8w\x05\xa6\xc1" +
-	"\xe8f\x00\xbeKB\xfe\x0cC\x94\x9c`\x9f$\xc51" +
-	"\x09\xf9\xab\x0c\xe5\x08:\xc1\xfey-\x00? !?" +
-	"\xcaP\xaea6\xe7\xc9G(\xa5\x87$\xe4g\x18Z" +
-	"f\xd1\xd4\xf2\x9d\x9a\x09\x09Q\xe8\x19\xf6\xa8\xdb~\xbd" +
-	"\xbcX\x0240\x0a\x0c\xa3\x80\xd6\x80^\xd0\x07\xb4|" +
-	"\x07j\x85\xdcz=g\xf6\x03x\xb3\"\xef\x04&H" +
-	"\xdf\xab:\x0f\x1e\xb7\xea4\xa3\xb3H\xdd\x0e\xb1b!" +
-	"\xa0\xe6\x01\xe7\xa8\xa5h\xb0\x06\xc4\x1e\x94\xa1\xda\x95\x9c" +
-	"\xa9\xe2\xb6n\xa5\xc1\x0d3\xdc\x187\xbau5\x8by" +
-	"\x8d\xbe\x12\x12\xc3%\xff\x8c\x84e\xf6\xbd\xfa\x7f\xb3\xe7" +
-	"f\x0eOv\x86\xd3\xb8n\xdf.)\x98e\xb4\xc7`" +
-	"\xbdw\xca\x12\x9a\xcf\x8b%\xe47\xf8\x83|M\xc6\x1f" +
-	"\xee^\xeaD\x87?\xdd\xbf\xdc\\\xb6L}@\x18\xa6" +
-	"6\x00X\xaa\xe0\xfd\x05\xb3zy\xd1H\x99\x04\xc9\xb9" +
-	"V\x1a\x0c\xecl\xb4\xd9\xb0D\xa9X\xf6\x86wJ\xcb" +
-	"\xe5\xca\xc6\xe4\x1d\x90\xd2\x9a\x97\x89\"\x8f`\xe0J " +
-	"c\xc7\xc8\xb2\xee\xecb\xcd\xd4\x82\xa7\xde\xe66\xdcb" +
-	"\x1f\x9av*\xc1+$\xe4\xcb\x19Z\xe5\xe2\xa0)\xca" +
-	"\x9dEt&\x81\x01>\x14\x9ei\xb74\xaa\xafYR" +
-	"\xd89\xdb\xb7\xe6e\xdd)\xdb\x97P\xa6V\xb8\x99\xea" +
-	"\x0e\x10Z\x17\xa5\xaaSB~-C\xea<\xca\xd4*" +
-	"\xcaT\xb7\x84|5C+\xaf\x99\xba9\x98\x13DH" +
-	"u\xc0\xb0\x8ej\xbeX\xe8\xa3\x97\x80\xa2\xf2n\x84P" +
-	"\x13\x86Q\xd9\x0bC\xf8\x05\x0a)Qe\x95:'\x85" +
-	"{\x17\xc0P\x89b%\xf8\x04\xb1 YT=\x8b\xb7" +
-	"\xd2*\xb9\xc1\xdd\x1a+\xb1n\x9c\xe1o\x8d2\xabu" +
-	"\x82\xddD\xa8\xdc.!\xbf\x8b\x18\x05\x03\x97^\xf9\xc7" +
-	"\xad\xc00\xe2P\xc7 \x91gIB\xfe#\x86\xb1\x9c" +
-	"aVR\x113\xca=^Z\x06\xb4\x0d\x94\x0b\x83\xb0" +
-	"\xaaTSo^\xeb3\xda\xfaK\x8bz\xfb\x021M" +
-	"_\xf2\xeeU\xca\x1f\xbf\xbaw\xf2\xb1\xe46\\\xcc," +
-	"\x87\x1b\x8ejy\xa1\x84\xbc3\x10Z\x9a\xa2X.!" +
-	"_I\xa1\xb9y\xe47\xfbyL\xd0J\x83I\xff\xef" +
-	"\x0dw\xa2\xf4\x17\x0d\xd3\x9f7\xde\xe5%\xb0\x07eM" +
-	"\xcd\x846\xbd\x87\xb40\xe9_\xf6\xaa\x8e\xa4@\x9e%" +
-	"''\x81,\xcf\xf1\x17\xe6\x849\\\x12\x98\xb0\xbe{" +
-	"\xf9Cub\xf4\xd4v\xb2\x96\x98\x90\xdb\xf6l\xba\xcd" +
-	"\xa1\xb7In<*\x9b\xac3\"\x13;\xa32\x8c\x9c" +
-	"Y\x14\x1b.\x89\x10\xb0\xe5j\xfdq\xa3\xdf\x1f\x1e\x95" +
-	"\xadj\x0d4\x88Me\x9dz\x01R\xf6\x1c\xf0\x06I" +
-	"I\xd8\xafo\x82D\xf0\xf59\x1b\xf9,Jw\xf8V" +
-	"\x9a\x90\xfe\xb5\xd5\xbc\x9c\xe1\xa7\xbf\xd2\xc5<\xe3:\x99" +
-	"\x0f\xd2\x7f\xcc\xc1=H\xfb\x09\xc0\x98i\xe6=F\xf5" +
-	"J\"8\xd1\x82\x95\xd10\xe9\xf5\xf4\x7f^\xcf\xbc\x1b" +
-	"\xfc\x17\x99M\xd1\xec\x19\xfe\x12\x17Uj\x84\x8b%\xe4" +
-	"\x97\xb3\xd0Jv\xeeJ\x9f@*m\xfd\xde\x9d/p" +
-	"b\xc6_\xa1*'\xb6t\xb8'\x12\xa3\x8br\xb9X" +
-	"^T\xb4\x99\xd1\xcd\xf8\xc4\xa0\xbd\x7fp\xaa\x06\x1d(" +
-	"\x82\xaa\xd7\xces\xe2\xe9\xfd\xd7R\xd5\xf4r\x17\x82f" +
-	"-\x17\xcb\x95\x0d'0'\x8a \x96vY\xb1\xd0\xb6" +
-	"\x9b\xd0KC\x97V\xb6s\xfa\xf1\xf5/^\xd5\xfd\xa4" +
-	"\x05\xea\xb75H_\x11\x97\xbef\x04\x8a\x9au;\xa7" +
-	"w-\xf0\x8b\xfa\xec\xb6\x09^\xf1\xdbtcQ\xb1," +
-	"*\x9b\xeb\x7f\x03\x00\x00\xff\xff`M\x86\xd5"
+const schema_8f4bd412642c9517 = "x\xda\x94X{l\x1c\xe5\x11\x9f\xf9\xf6|\xe7\xd8>" +
+	"\xefmv\x0d\x91+qm\x0a\xcaC\xc4\xc2\x0e\x14\x88" +
+	"(N\x9c\xa7S\x07\xbcw\x09E(Q\xd9\xdc}\xb6" +
+	"\x17\xcew\x97\xdb\xb5\x13#\xa8\x1bDZ\x926\x02\x0a" +
+	"QI!j\x02\x04\xe26\xa8\x84\xba<\xd2\x16\x15\x01" +
+	"m\x89R\x1e\x15\x12j\xc4#\xa4\x04\x12 \x12\x0e\xa1" +
+	"!i\xe8V\xb3\xbb\xb7\xbbY\xaf\x03\xfdoogv" +
+	"\xbe\x99\xdf\xcc\xfcf\xbe\xbb\xe4\xe1\xf8\\\xd6Z\xf3\xd8" +
+	"$\x00\xb5\\\x13\xb7>{\xe2\xf1G?>q\xcbO" +
+	"@J\xa2u\xfe\x96\x8b\xf3\x93\xdf\xf8\xde]P\x83\x09" +
+	"\x00\xf9\xd9\xd8\x01\xf9\xa5\x18==\x1fk\x07\xb4f\xbd" +
+	"\xf3\xdd]\xc3\xd3F\xef\x04\xb5\x19\xd1:|\xd9\xfc\xa5" +
+	"\xad'_\xdd\xe9*\x8f\xc5N\xcb_\xda\xca\xa7bk" +
+	"\x01\xad\x13\x07N\xfd\xe0\xb9}\xefn\x025\x89A\xcb" +
+	"\x8cTV\xd4\xec\x93\xb5\x9a\x04\xc0\xecU5i\x04\xb4" +
+	"\x9a\xa5\x07\x17\xbd_Y\x7fWH\xdb\xb6\xb7!\xfe\xa4" +
+	"\xbc9NO\x1b\xe3\xe4\xc6\xa2\x17\x16\x0d\x8f>\xf0\xd1" +
+	"=\xa4\xcb|\xdd\x85,\x91\xc4\x98<\x12\xdf+\xef!" +
+	"\xed\xd9\x8f\xc7\xaf\x15\x00\xadmG\x94C\xd3\xa7\xfc\xf0" +
+	"\xbe\xa8\x08;\xeb\xf6\xc9+\xea\xe8I\xad#\xd3;n" +
+	"\xab\xdfu\xd9\xdc\xa1-!\xd3\xb6\x1b\xeb\xeb\x0e\xc8\x9b" +
+	"m\xdd\x8du\x14\xe0\xd1\x8ew7<\xb6!\xfe@\x94" +
+	"\xdd\xc3u\x1f\xc9c\xb6\xee1\xdb\xee\x81\x7fn:\xf2" +
+	"^\xcd+\x0f\x80\xda\x84\x82\xf5\xf1\xc3/\xbe\xd5\xda\xf4" +
+	"\x97\x17\xa1\x09\x13\x08 K\xf5\x07\x00\xe5\xa6z\xb2\xea" +
+	"\xa1\x1a\x82M ck\xea\xff!\xdfV\x7f9\xc0\xec" +
+	"\x9d\xf5\xdf'\xd8&\xb7no]Y{\xedH\x84\x0f" +
+	"\xb3\xafL2\x94\x17&\xe9\xbbyIrbtlD" +
+	"\xbda\xca\x17\xbb\xc3\xa6m\xed\x81\xe4d\x947\xd8\xda" +
+	"\xeb\x93\xbf\x05\xb4\xbey\xd1\xbdkk\xa65?\x19\x99" +
+	"\xbf\xd6\xc6'\xe5+\x1b\xe9\xe9\xb2F\xf2\xdazu\xff" +
+	"\xf5G\xb7\xdc:\x1aY\x19\x03\x8d\xc7\xe5\xf5\x8d\xe7\x13" +
+	"p\xb6r\xdb\xfb\xb5\xdf6\xdey\xe5iP/\xc0\xc0" +
+	"\xa7\x8e\x1f\xa7\x1a\xdbP\x9e$\xd2w5\"\xf9q\xe4" +
+	"\xf8y\x83\x87\x8f\xcd}!\x0a\xe6\x9d\xe2G\xf2\x1e[" +
+	"\xf7q\x91\"\xf4\x80U\x93(\x84\x95\xdf\x16\x7f-\x1f" +
+	"&\xe5\xd9\xef\x89v\xcd}2\xf8\x8b\xf2\xf2\x16\xeb\xa5" +
+	"\x90e\x1b\xe9\xa3\xa9C\xf2\xe7)\xbb\xaeS\xe4\x85\xc8" +
+	"_\x9d\xd7q\xc7\x05\xfb\xa2\xea\xf3\x1e\xe9\x80\xbcM\xa2" +
+	"\xa7\xad\x12y\xb1\xf3\x83\x0b\x1f\xdc\xf5\x10\xdf\x1f\xa5\xfb" +
+	"\xac\xb4W~\xde\xd6\xfd\x93\xad\xbbJZ\xf9\xf4\xe0\xf4" +
+	"\x97\xdf$\xe0\x98\x0f\x1c\xe9\xce~[Z\x8d\xf2\x98\xad" +
+	"|L\"'\xdez\xef\x0f\x8fn\xbcw\xda\x87\x91\x09" +
+	"\xdc<\xb9\x19\xe5m\x93m7&\x93v\xe1`\xe6\xba" +
+	"\xe6\xd7O~\x18Y\xf7\xf2>y\x85l\xd7\xbdLn" +
+	"\\1\xed\xcd\x1f\xf76\xbd\xf4i\x94ey\xbd|\\" +
+	"\xdel+o\x94)\x7f\xed\x1f\\=\xe3\xa9\xa3\xe2X" +
+	"\xa4\xf2Qy\xaf<f+\x1f\xb3\x95\x9f}n\xdd\xc8" +
+	"O\xdf|\xf4d\x94\x17\xab\x94\xe3\xb2\xae\xd0\x13W\xc8" +
+	"\x8b\x86\xe6w~\xd3{\xd1\xe1S\xa0\x9e\x87\x81\x02l" +
+	"bv\x97lQ\x0e\x01\xca[\x15\xb2\xfa\xbb\xa7nY" +
+	"<\xfa\xf0\x9e\xd3Q}\xfa\xb9r\\\xc6&z\xfaR" +
+	"!\x1c\x8c\x9c^*\xe6[rL+\x17\xcbs:\x17" +
+	"u\x16{J\x19\xbef\x80\x0b\x86\xd9\x8d\xa8\xc6\x84\x18" +
+	"@\x0c\x01\xa4d\x1b\x80Z+\xa0z!\xc3\xb4\xde\xd3" +
+	"\xb9\xc0\xc0F\xc0n\x01q\x120z\x0c\xd9\xea\xd6\xcc" +
+	"\xbee\xdc\xd4\xf2\x9a\xd9\xae\xb5\\S29\xd9\xab\xf5" +
+	"\xec\xcd\x98\x09\xa0^(\xa0z\x09CD\x05\xe9\xdd," +
+	":c\xba\x80\xea\xa5\x0c\xc5b\xc9\xe4\xd8\x00\x0c\x1b\x00" +
+	"\xd3\xba\x91\xd7\x0c\xfb\xa4I\xe3NZ\xb46_=\x0c" +
+	"\x80\x0eQ\xbcCn\xeb\x00P\xd7\x09\xa8\xde\xe1\x1f\xb2" +
+	"~*\x80z\xab\x80\xea\x9d\x0c%\x86\x0a2\x00i\xc3" +
+	"\x0d\x00\xea\x1d\x02\xaa\xdb\x19J\x02*(\x00H\xdb\xe8" +
+	"\xeb\xfb\x05T\x1fa(\xc5R\x0a\xc6\x00\xa4\x1dK\x01" +
+	"\xd4\xed\x02\xaa\xbb\x19\x0e\xf78Gc\x12\x18&\x01\x13" +
+	"\xfd\xe6\x00&\x80a\x02\xd0\xd2\x8b&\xaf\xf4h9\x10" +
+	"\xb8\x07U\xca'Q@z9\xcc\xd7\x95\x97\xeb\xfd\x1c" +
+	"k\x81a-\xa0\xd5\xc7\xb5<\xaf\\\xd7\x06\xa4\x00\x0c" +
+	"1\x10.\xda\xe1f\xf8`:\xc3\xcb\x85\xa1P~\xe6" +
+	"\xb8\xf9Q\x18\xb6W\xb81P0=W\xce6\x90\x9d" +
+	"\xdf\xd9~\xed5\x0b\x96\x19\xbdd\xe1\xaa\xaa\x05y\x0b" +
+	"6\x03d\xefF\x01\xb3\x0f\"\xc3$Z\x96\x8d\x98\xbc" +
+	"\x15\xdb\x00\xb2\xf7\x91`;\x09\xd8\x7f-\x1b5y\x1b" +
+	"v\x00d\xef'\xc1#$\x10\xbe\xb4l\xe4\xe4\x1d\x98" +
+	"\x01\xc8n'\xc1n\x12\xc4\xceX6z\xf2\x88-\xd8" +
+	"E\x82Q\x12\xd4\xfc\xc7R\xb0\x06@\xde\x83\xab\x01\xb2" +
+	"O\x90\xe0\x8f$\x88\x9f\xb6\x14\x8c\x13)\xe0\xed\x00\xd9" +
+	"gH\xf0\"\x09\x12\xa7,\xc5n\x90\xe7\xb1\x02\x90\xfd" +
+	"3\x09\xf6\x93\xa0\xf6\x0bK\xc1Z\x00\xf9e\xdb\xd4\xdf" +
+	"H\xf0\x06\x09&\x9d\xb4\x14\x9c\x04 \xbf\x8e\xbf\x04\xc8" +
+	"\xbeA\x82\x83$\xa8\xfb\xb7\xa5`\x1d\xb1\x1fn\x02\xc8" +
+	"\x1e$\xc1'$\xa8\xff\xdcR\xb0\x9e\x1a\x16\x97\x02d" +
+	"\x8f\x90\xe0\x04\x09\x1aNX\x0a6\x10\xf1\xd9\x87\x7fJ" +
+	"\x823$H~f)\x98\xa4\xf9n\xbb\xfb\x05\x09b" +
+	"\x8c\xa1\xd4\x88\x0a6\x02\xc8\xc8\x08\xaa3\xf4\xbe\x961" +
+	"\x14\xf4|\xb5\x9a\xd3\x03E\x83\x9b\x10\x1f.kf_" +
+	"\x86\xaf\xc1\x94O\xd5\x80\x98\x02\xb4\x1cI\xb9\x008\x84" +
+	")\x9fb\\\xa9f8]\x0bH\xdfz\x04\x1b\x96&" +
+	"\xca\x05\xfa\xda\x9b\xf8\xae\xbc\xc2\x07\xaf)\x99z\x0f\xea" +
+	"9\xcd\xd4KE\xc0\x94?\xbd]\x1d\xbd\xc7\xb5\x91^" +
+	"3\xc0\x0d\x13S\xfef\x14\xd6pO\xf1\xf8\xd5\x95\x1b" +
+	"\xbc2\xa8\xe7x'\x06\xf8\x05S\xfe\x88\x8eT+\x17" +
+	"\x86\x80\xdc\xf1h\xd2w\xd9\x15\x92\xd4\xdb\x8e<\x1b\xbd" +
+	"\xcb\x87\xca|\x09\xa4Ke\x07No>\x854\x90\x14" +
+	"\xc8\x0e\xa6\xfcI\xea\xe8\x0c\x9b\x15-\xc7;\xf3\xd5\xce" +
+	"\x0eQ\xce\xbcl\xa7\xefa\xa8\x0f;|\x9e\x1c\xe6E" +
+	"\xb3\xa2\x07\xdb\xdf\xe3f\xa7\xfd#8\xb3\xd3\xa1\x0d!" +
+	"\x17\xe6\xcb6\x9f/%\x8f0g\xfa\x84y6G\x8a" +
+	"D\xd1\x13\x10f\xc6M\xb9\x9e\xd3DJy(\x00\"" +
+	"\xb8\x06\x01\xd5)\x0c-#\xc3\x07)T\x07\xea\xcc\xbf" +
+	"N_\xbeaq\xdb\xaf< \xc7y\x9f\xe1kZz" +
+	"\x0a\x9a\xd0k\x90\xeb\xa9\xbb\x1d\"\x9d\xd1\x11\xf4\xfd\x1e" +
+	"\x87Hg\xcd\xf1}\x1f\xae\xf0\x9e\x0a7\xfa\xaa\x94\xd7" +
+	"\xde\xa7\xe7\xf3\xbc8\x8e\x01\xc7\x8d\x16\xd4\xd4\x06\x0cl" +
+	"\x0a\x92\xf4s\x7fi\x92\x9a\xa6\xfak\xb9$\xcd\xb4\\" +
+	"ls\xc8\xbb\xf4\xe2\xcd\xcb\x87\xca\xc8\x13\x8byI\xa4" +
+	"\xe9\xa4N\xf1\x10\xd8zS\x80\xf7\x919P\xef\xc8\xf8" +
+	"\xbc\xef\x8d\x8d\x91M\x00\xean\x01\xd5g\x18\xa2\xe0\x04" +
+	"\xfb{R\x1c\x15P}\x8d\xa6\x06:\xc1\xfe}5\x80" +
+	"\xba_@\xf5\x08C\xa9\x86\xd9\x9c'\x1d\xa6\x94\x1e\x14" +
+	"P=\xc3\xd02K\xa6V\xe8\xd2L\x10y17\xe4" +
+	"Q\xb7\xfdzI\xa9\x0ch`\x1c\x18\xc6\x01\xad~\xbd" +
+	"\xa8\xf7k\x85\x0e\xd4\x8a\xf9\xb5z\xde\xec\x03\xf0\xe6G" +
+	"\xc1\x09\x8c\x93\xbeWu\x1e<n\xd5iFW\x89\xba" +
+	"\x1d\x12\xa5b@\xcd\x03\xceQK\xd3\x04\x0e\x88=(" +
+	"C\xb5+8S\xc5m\xddj\x83\x1bf\xb81nr" +
+	"\xebj:\xf3\x1a}9\x88Ce\xff\x0c\xd12{_" +
+	"\xfb\xc6\x8cY\x99C\x13\x9d\xe14\xae\xdb\xb7\x0b\x8bf" +
+	"\x05\xed1\xd8\xe0\x9d\xb2\x90\x06\xf9\x02\x01\xd5\x1b\xfd\x89" +
+	"\xbf\x8a2\xb2R@\xb5/\x90:N5y\xa3\x80j" +
+	"\x81}\xcdYm\x99z?7L\xad\x1f\xb0\\\xc5{" +
+	"\xdc\xfc>{\xd4.)\x19i\x93 9\xd7\xee\x83\x81" +
+	"\xe5\x8eV &\x96K\x15ox\xa7\xb5|\xbebL" +
+	"\xdc\x01i\xade1/\xa91\x0c\xdc\x1d$\xec\x18^" +
+	"\xdc\x9d]\xa0\x99Z\xf0\xd4\xdb\xdd\x86[\xe0C3\x8f" +
+	"J\xf0*\x01\xd5%\x0c\xadJi\xc0\xe4\x95\xae\x12:" +
+	"\x93\xc0\x00\x1f\x0a\xcf\xb4[\x1a\xd1\xfb\x98\x10v\xce\xf6" +
+	"\xadeqw\xda\xf6%\x94\xa9\xa5n\xa6\xba\x03\x84\xb6" +
+	"\x8cR\xd5%\xa0z=C\xea<\xca\xd4\x0a\xcaT\xb7" +
+	"\x80\xeaJ\x86VA3us \xcf\x89\x90\xea\x80a" +
+	"\x1d\xd5|\xa9\xd8K/\x01y\xf5\xdd0\xa1\xc6\x0d\xa3" +
+	"\xba@\x86\xf0\x0b\x14\x92\x18\xb1J\x9d\x93\xc2\xbd\x9bb" +
+	"\xa8D\xb1\x1a\xbcH,\x18\xdaC\xa7\xfa{\xa8\x14\xbd" +
+	"\x88\xd6\xba\x8b\xe8Rw\x11\xbd\x9b\x18\x05\x03\xb7ci" +
+	"s\x1b0\x8c9\xd41@\xe4Y\x16P\xfd\x19\xc3D" +
+	"\xde0\xab\xa9H\x18\x95\x9c\x97\x96~m\x1d\xe5\xc2 " +
+	"\xac\xaa\xd5\xd4S\xd0z\x8d\xf6\xbe\xf2\xfc\x9e\xde@L" +
+	"S\x16\xbe\x7f\xb5\xfc\xd7o\xed\x9dx,\xb9\x0d\x970" +
+	"+\xe1\x86\xa3Z\x9e+\xa0\xda\x15\x08\xad\x93\xa2X\"" +
+	"\xa0\xba\x9cBs\xf3\xa8\xde\xe2\xe7Q\xa4\x95\x06S\xfe" +
+	"\x1f!\xeeD\xe9+\x19\xa6?o\xbc[N`\x0f\xca" +
+	"\x9a\x9a\x09\xedz\x8e\xb40\xe5\xdf\x0a#GR \xcf" +
+	"\x82\x93\x93@\x96g\xfa\x0b\xb3h\x0e\x959\x8a\xd6\x8f" +
+	"\xaex\xa8\x8e\x8f\x9c\xdcA\xd6\xc4q\xb9\x9d\x97\xedl" +
+	"w\xe8m\x82\xab\x91\xc2&\xea\x8c\xd8\xf8\xce\xa8\x0e#" +
+	"g\x16%\x86\xca<\x04l%\xaa?n\xf2\xfb\xc3\xa3" +
+	"\xb2\x15m\x81\x06\xb1\xa9\xacK/B\xda\x9e\x03\xde " +
+	")s\xfb\xf5\xcd \x06_\x9f\xb3\x91\xcf\xa2t\x87o" +
+	"\x85q\xe9_\x1d\xe5\xe5T?\xfd\xd5.V3\xae\x93" +
+	"\x85 \xfd'\x1c\xdc\x83\xb4/\x02&L\xb3\xe0\xdf\x88" +
+	"\xaa%\x11\x9ch\xc1\xcah\x9c\xf0\x1e\xfb\x7f\xafg\xde" +
+	"U\xff\xab\xcc\xa6i\xf6\x0c}\x8d\x1b-5\xc2\xc5\x02" +
+	"\xaaW\xb0\xd0Jv\xeeJ\x1fG*\xed}\xde\x9d/" +
+	"pb\xc6_\xa1\xaa'\xb6v\xb8'\x12\xa3\xf3J\xa5" +
+	"T\x99_\xb2\x99\xd1\xcd\xf8\xf8\xa0\xbd\xbfz\"\x83\x0e" +
+	"\x14A\xe4\xb5\xf3\x9cxz\x7f\xcaD\x9a^\xe2B\xd0" +
+	"\xa2\xe5\x13\xf9\x8a\xe1\x04\xe6D\x11\xc4\xd2.+\x16\xda" +
+	"vE\xbd<xiu;\xa7\x1f\xdf\xf9\xeaU\xddO" +
+	"Z\xa0~\xdb\x82\xf4\x15s\xe9kj\xa0\xa8Y\xb7s" +
+	"\xfa\xb29~Q\x9f\xdd6\xc1k\x7f\xbbn\xcc/U" +
+	"xus\xfd_\x00\x00\x00\xff\xff\x0c\x9d\x98\x01"
 
 func init() {
 	schemas.Register(schema_8f4bd412642c9517,
diff --git a/proto/sciond.capnp b/proto/sciond.capnp
index cf1375bc6..bc9a8e53a 100644
--- a/proto/sciond.capnp
+++ b/proto/sciond.capnp
@@ -94,6 +94,7 @@ struct FwdPathMeta {
     mtu @1 :UInt16;
     interfaces @2 :List(PathInterface);
     expTime @3 :UInt32; # expiration time in seconds since epoch.
+    headerV2 @4 :Bool;
 }
 
 struct PathInterface {


Test patch:
diff --git a/go/cs/beacon/beacondbtest/beacondbtest.go b/go/cs/beacon/beacondbtest/beacondbtest.go
index bb5fa02c5..e42356726 100644
--- a/go/cs/beacon/beacondbtest/beacondbtest.go
+++ b/go/cs/beacon/beacondbtest/beacondbtest.go
@@ -775,6 +775,7 @@ func AllocBeacon(t *testing.T, ctrl *gomock.Controller, ases []IfInfo, inIfId co
 			RawInfo:      rawInfo,
 			RawTimestamp: infoTS,
 			SegID:        10, // XXX(roosd): deterministic beacon needed.
+			ISD:          1,
 		},
 	)
 	require.NoError(t, err)
diff --git a/go/cs/beaconing/extender_test.go b/go/cs/beaconing/extender_test.go
index d38fd1599..8df110032 100644
--- a/go/cs/beaconing/extender_test.go
+++ b/go/cs/beaconing/extender_test.go
@@ -146,6 +146,7 @@ func TestLegacyExtenderExtend(t *testing.T) {
 					RawInfo:      rawInfo,
 					RawTimestamp: ts,
 					SegID:        uint16(mrand.Int()),
+					ISD:          1,
 				},
 			)
 			require.NoError(t, err)
diff --git a/go/lib/infra/modules/combinator/expiry_test.go b/go/lib/infra/modules/combinator/expiry_test.go
index b9048a41c..7c95bfac6 100644
--- a/go/lib/infra/modules/combinator/expiry_test.go
+++ b/go/lib/infra/modules/combinator/expiry_test.go
@@ -19,7 +19,7 @@ import (
 
 	. "github.com/smartystreets/goconvey/convey"
 
-	"github.com/scionproto/scion/go/lib/spath"
+	"github.com/scionproto/scion/go/lib/util"
 )
 
 // FIXME(scrye): when more unit tests get added to spath, these should probably
@@ -80,16 +80,12 @@ func TestComputeSegmentExpTime(t *testing.T) {
 func buildTestSegment(timestamp uint32, ttls ...uint8) *Segment {
 	segment := &Segment{}
 	segment.InfoField = &InfoField{
-		InfoField: &spath.InfoField{
-			TsInt: timestamp,
-		},
+		Timestamp: util.SecsToTime(timestamp),
 	}
 	for _, ttl := range ttls {
 		segment.HopFields = append(segment.HopFields,
 			&HopField{
-				HopField: &spath.HopField{
-					ExpTime: spath.ExpTimeType(ttl),
-				},
+				ExpTime: ttl,
 			},
 		)
 	}
diff --git a/go/lib/pathdb/pathdbtest/pathdbtest.go b/go/lib/pathdb/pathdbtest/pathdbtest.go
index 87e3da441..3df32e14f 100644
--- a/go/lib/pathdb/pathdbtest/pathdbtest.go
+++ b/go/lib/pathdb/pathdbtest/pathdbtest.go
@@ -319,6 +319,7 @@ func testUpdateIntfToSeg(t *testing.T, ctrl *gomock.Controller, pathDB pathdb.Re
 			RawInfo:      rawInfo,
 			RawTimestamp: 30,
 			SegID:        uint16(mrand.Int()),
+			ISD:          1,
 		},
 	)
 	require.NoError(t, err)
@@ -833,6 +834,7 @@ func AllocPathSegment(t *testing.T, ctrl *gomock.Controller, ifs []uint64,
 			RawInfo:      rawInfo,
 			RawTimestamp: expiration,
 			SegID:        uint16(mrand.Int()),
+			ISD:          1,
 		},
 	)
 	require.NoError(t, err)
diff --git a/go/lib/xtest/graph/graph.go b/go/lib/xtest/graph/graph.go
index 930d02795..74dfadcad 100644
--- a/go/lib/xtest/graph/graph.go
+++ b/go/lib/xtest/graph/graph.go
@@ -248,6 +248,7 @@ func (g *Graph) beacon(ifids []common.IFIDType, addStaticInfo bool) *seg.PathSeg
 			RawInfo:      rawInfo,
 			RawTimestamp: ts,
 			SegID:        uint16(rand.Int()),
+			ISD:          g.parents[ifids[0]].I,
 		},
 	)
 	if err != nil {