Skip to content

Commit 4843867

Browse files
SuyashAlphaCHayim.Shaul@ibm.com
authored andcommitted
feat(ttx): add versioned envelope for interactive protocol messages (#1700)
Signed-off-by: SuyashAlphaC <suyashagrawal862@gmail.com>
1 parent 184bde4 commit 4843867

3 files changed

Lines changed: 751 additions & 0 deletions

File tree

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package session
7+
8+
import (
9+
"context"
10+
"encoding/json"
11+
"fmt"
12+
"time"
13+
14+
"github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors"
15+
session "github.com/hyperledger-labs/fabric-token-sdk/token/services/utils/session"
16+
)
17+
18+
// CurrentVersion is the protocol version stamped on every outgoing envelope.
19+
// Receivers reject messages whose version differs from their own CurrentVersion.
20+
const CurrentVersion uint32 = 1
21+
22+
// Envelope wraps all TTX interactive protocol messages with version and type
23+
// information. Wire format uses compact field names for efficiency:
24+
// - "v" = Version (uint32, monotonic)
25+
// - "t" = Type discriminator (string, mandatory)
26+
// - "b" = Body (json.RawMessage, the actual payload)
27+
type Envelope struct {
28+
Version uint32 `json:"v"`
29+
Type string `json:"t"`
30+
Body json.RawMessage `json:"b"`
31+
}
32+
33+
// Validate checks that the envelope carries the expected message type.
34+
func (e *Envelope) Validate(expectedType string) error {
35+
if e.Type != expectedType {
36+
return errors.Join(errors.Errorf("expected %s, got %s", expectedType, e.Type), ErrTypeMismatch)
37+
}
38+
39+
return nil
40+
}
41+
42+
// Message type constants for all JSON-typed interactive protocol messages.
43+
const (
44+
// recipients.go
45+
TypeRecipientRequest = "recipient_req"
46+
TypeRecipientResponse = "recipient_resp"
47+
TypeExchangeRecipientRequest = "exchange_req"
48+
TypeExchangeRecipientResp = "exchange_resp"
49+
TypeMultisigRecipientData = "multisig_data"
50+
TypePolicyRecipientData = "policy_data"
51+
52+
// withdrawal.go
53+
TypeWithdrawalRequest = "withdrawal_req"
54+
55+
// upgrade.go
56+
TypeUpgradeAgreement = "upgrade_agree"
57+
TypeUpgradeRequest = "upgrade_req"
58+
59+
// multisig/spend.go and boolpolicy/spend.go
60+
TypeSpendRequest = "spend_req"
61+
TypeSpendResponse = "spend_resp"
62+
)
63+
64+
// Sentinel errors for envelope validation.
65+
var (
66+
ErrVersionMismatch = errors.New("protocol version mismatch")
67+
ErrUnsupportedVersion = errors.New("unsupported protocol version")
68+
ErrMissingVersion = errors.New("missing protocol version")
69+
ErrInvalidEnvelope = errors.New("invalid envelope format")
70+
ErrTypeMismatch = errors.New("message type mismatch")
71+
)
72+
73+
// VersionError provides structured detail about a version mismatch.
74+
type VersionError struct {
75+
Expected uint32
76+
Received uint32
77+
Message string
78+
}
79+
80+
func (e *VersionError) Error() string {
81+
if e.Message != "" {
82+
return fmt.Sprintf("protocol version mismatch: expected %d, received %d: %s", e.Expected, e.Received, e.Message)
83+
}
84+
85+
return fmt.Sprintf("protocol version mismatch: expected %d, received %d", e.Expected, e.Received)
86+
}
87+
88+
func (e *VersionError) Is(target error) bool {
89+
return target == ErrVersionMismatch
90+
}
91+
92+
// VersionCompatibility defines which protocol versions can communicate.
93+
// For v1, only same-version communication is supported.
94+
var VersionCompatibility = map[uint32][]uint32{
95+
1: {1},
96+
}
97+
98+
// IsCompatible returns true if local and remote versions can interoperate.
99+
func IsCompatible(local, remote uint32) bool {
100+
compatible, ok := VersionCompatibility[local]
101+
if !ok {
102+
return false
103+
}
104+
for _, v := range compatible {
105+
if v == remote {
106+
return true
107+
}
108+
}
109+
110+
return false
111+
}
112+
113+
// WrapEnvelope marshals v into an Envelope with the given message type.
114+
func WrapEnvelope(v any, msgType string) (*Envelope, error) {
115+
body, err := json.Marshal(v)
116+
if err != nil {
117+
return nil, errors.Wrap(err, "failed to marshal envelope body")
118+
}
119+
120+
return &Envelope{
121+
Version: CurrentVersion,
122+
Type: msgType,
123+
Body: body,
124+
}, nil
125+
}
126+
127+
// UnwrapEnvelope decodes raw bytes into an Envelope and validates version
128+
// and (optionally) message type. If expectedType is non-empty, the type is
129+
// checked; pass "" to skip the type check.
130+
func UnwrapEnvelope(raw []byte, expectedType string) (*Envelope, error) {
131+
var env Envelope
132+
if err := json.Unmarshal(raw, &env); err != nil {
133+
return nil, errors.Join(errors.Wrap(err, "invalid envelope format"), ErrInvalidEnvelope)
134+
}
135+
if env.Version == 0 {
136+
return nil, ErrMissingVersion
137+
}
138+
if env.Version != CurrentVersion {
139+
return nil, &VersionError{Expected: CurrentVersion, Received: env.Version}
140+
}
141+
if len(env.Type) == 0 {
142+
return nil, errors.Join(errors.New("type field is empty"), ErrInvalidEnvelope)
143+
}
144+
if expectedType != "" {
145+
if err := env.Validate(expectedType); err != nil {
146+
return nil, err
147+
}
148+
}
149+
150+
return &env, nil
151+
}
152+
153+
// UnwrapBody is a convenience that unwraps an envelope and unmarshals the body
154+
// into dst in a single call.
155+
func UnwrapBody(raw []byte, expectedType string, dst any) error {
156+
env, err := UnwrapEnvelope(raw, expectedType)
157+
if err != nil {
158+
return err
159+
}
160+
161+
return json.Unmarshal(env.Body, dst)
162+
}
163+
164+
// SendTyped wraps v in a versioned envelope with the given message type and
165+
// sends it over the session.
166+
func SendTyped(s *session.S, ctx context.Context, v any, msgType string) error {
167+
return SendTypedWithMetrics(s, ctx, v, msgType, nil)
168+
}
169+
170+
// SendTypedWithMetrics is like SendTyped but also records envelope metrics.
171+
func SendTypedWithMetrics(s *session.S, ctx context.Context, v any, msgType string, m *EnvelopeMetrics) error {
172+
env, err := WrapEnvelope(v, msgType)
173+
if err != nil {
174+
return err
175+
}
176+
m.observeSend(msgType, len(env.Body))
177+
178+
return s.SendWithContext(ctx, env)
179+
}
180+
181+
// ReceiveTyped receives a versioned envelope, validates its version and type,
182+
// and unmarshals the body into dst.
183+
func ReceiveTyped(s *session.S, expectedType string, dst any) error {
184+
return ReceiveTypedWithTimeout(s, expectedType, dst, session.DefaultReceiveTimeout)
185+
}
186+
187+
// ReceiveTypedWithTimeout is like ReceiveTyped but with an explicit timeout.
188+
func ReceiveTypedWithTimeout(s *session.S, expectedType string, dst any, d time.Duration) error {
189+
return ReceiveTypedWithTimeoutAndMetrics(s, expectedType, dst, d, nil)
190+
}
191+
192+
// ReceiveTypedWithTimeoutAndMetrics is like ReceiveTypedWithTimeout but also
193+
// records envelope metrics.
194+
func ReceiveTypedWithTimeoutAndMetrics(s *session.S, expectedType string, dst any, d time.Duration, m *EnvelopeMetrics) error {
195+
raw, err := s.ReceiveRawWithTimeout(d)
196+
if err != nil {
197+
return err
198+
}
199+
200+
env, err := UnwrapEnvelope(raw, expectedType)
201+
if err != nil {
202+
m.observeError(classifyError(err))
203+
204+
return err
205+
}
206+
m.observeReceive(env)
207+
208+
return json.Unmarshal(env.Body, dst)
209+
}
210+
211+
func classifyError(err error) string {
212+
switch {
213+
case errors.Is(err, ErrMissingVersion):
214+
return "missing_version"
215+
case errors.Is(err, ErrVersionMismatch):
216+
return "version_mismatch"
217+
case errors.Is(err, ErrTypeMismatch):
218+
return "type_mismatch"
219+
case errors.Is(err, ErrInvalidEnvelope):
220+
return "invalid_envelope"
221+
default:
222+
return "unknown"
223+
}
224+
}

0 commit comments

Comments
 (0)