Skip to content

Commit 814dad0

Browse files
authored
token: introduce policy-based identity (#1586) (#1597)
Signed-off-by: Siddhi Khandelwal <siddhi.200727@gmail.com>
1 parent f96ff8b commit 814dad0

31 files changed

Lines changed: 3191 additions & 16 deletions

File tree

cmd/tokengen/testdata/zkatdlognoghv1_pp.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/services/identity.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,24 @@ Located in `token/services/identity/multisig`.
232232
* **Usage**: Useful for requiring multiple signatures or representing a group of parties.
233233
* **Auditability**: Aggregates audit information for all underlying identities.
234234

235+
#### PolicyIdentity (Boolean-Expression-Governed Ownership)
236+
Located in `token/services/identity/boolpolicy`.
237+
* **Concept**: An identity whose ownership is governed by a boolean expression over a set of component identities, enabling OR-style (any one signer suffices) and AND-style (all signers required) multi-party control without a fixed M-of-N scheme.
238+
* **Policy Expression Syntax**: A string using `$N` slot references and the operators `AND`, `OR`, and parentheses:
239+
- `$0 OR $1` — either component identity 0 or 1 can satisfy ownership alone.
240+
- `$0 AND $1` — both component identity 0 and 1 must sign.
241+
- `($0 OR $1) AND $2` — one of the first two parties plus the third must sign.
242+
* **Identity (Payload)**: An ASN.1-encoded `PolicyIdentity` sequence:
243+
- `policy` (UTF8String): the boolean expression, e.g. `"$0 OR $1"`.
244+
- `identities` (SEQUENCE OF OCTET STRING): ordered list of raw component identity bytes; `$N` indexes into this list.
245+
* **Audit Info**: JSON-encoded `AuditInfo` structure.
246+
- `IdentityAuditInfos` (array of `IdentityAuditInfo`): per-component audit info blobs in the same order as `identities`.
247+
* **Encoding**:
248+
- `TypedIdentity` payload: ASN.1 DER.
249+
- Audit Info: JSON.
250+
* **Signature Representation**: An ASN.1 `PolicySignature` (`SEQUENCE OF OCTET STRING`) where each slot corresponds to one component identity. A slot may be nil/empty when that component does not need to sign (valid for OR branches).
251+
* **Implementation**: `token/services/identity/boolpolicy`.
252+
235253
#### HTLC (Hashed Time Lock Contract)
236254
Located in `token/services/identity/interop/htlc`.
237255
* **Concept**: A script-based identity used primarily for interoperability mechanisms like atomic swaps.
@@ -258,3 +276,111 @@ Typical extension scenarios include:
258276
- Supporting a new identity type by implementing a custom `KeyManager`
259277
- Customizing signature generation or verification logic within a `KeyManager`
260278
- Providing a custom `KeyManagerProvider` to plug new identity mechanisms into `LocalMembership`
279+
280+
### Step-by-Step Guide: Introducing a New Identity Type
281+
282+
The steps below describe how to add a new composite identity type end-to-end, based on the pattern used for **PolicyIdentity** (`token/services/identity/boolpolicy`).
283+
284+
#### Step 1 — Reserve a type tag
285+
286+
Add a new constant to `token/driver/wallet.go` alongside the existing tags:
287+
288+
```go
289+
const (
290+
// ...existing tags...
291+
MyNewIdentityType IdentityType = 7
292+
MyNewIdentityTypeString = "mynew"
293+
)
294+
```
295+
296+
The integer must be unique across all registered identity types.
297+
298+
#### Step 2 — Define the wire format
299+
300+
Create a package (e.g. `token/services/identity/mynew/`) and define the identity struct. Use ASN.1 DER for structured binary data (as PolicyIdentity does) or JSON for human-readable payloads (as HTLC does):
301+
302+
```go
303+
type MyNewIdentity struct {
304+
SomeField string `asn1:"utf8"`
305+
Parts [][]byte
306+
}
307+
308+
func (m *MyNewIdentity) Serialize() ([]byte, error) { return asn1.Marshal(*m) }
309+
func (m *MyNewIdentity) Deserialize(raw []byte) error {
310+
_, err := asn1.Unmarshal(raw, m)
311+
return err
312+
}
313+
```
314+
315+
Expose `Wrap` / `Unwrap` helpers (see `boolpolicy.WrapPolicyIdentity` / `boolpolicy.Unwrap`) that embed the serialized struct inside a `TypedIdentity` envelope with the new type tag.
316+
317+
#### Step 3 — Implement signature verification
318+
319+
Add a `Verifier` that accepts the new signature format and a `Deserializer` that reconstructs a `Verifier` from raw identity bytes. Register the deserializer via `des.AddTypedVerifierDeserializer(mynew.MyNewIdentityType, ...)` in each driver's `NewTokenService` (see `token/core/fabtoken/v1/driver/driver.go` and the zkatdlog equivalent).
320+
321+
#### Step 4 — Define the signature format
322+
323+
Define a struct for the signature produced over the token request (analogous to `PolicySignature` in `boolpolicy/sig.go`). Include ASN.1 or JSON encoding helpers and a `JoinSignatures` function if multiple parties contribute partial signatures.
324+
325+
#### Step 5 — Implement the `Authorization` checker
326+
327+
Create an `EscrowAuth` struct (see `token/services/ttx/boolpolicy/auth.go`) that implements the `Authorization` interface:
328+
329+
```go
330+
type EscrowAuth struct{ WalletService driver.WalletService }
331+
func (a *EscrowAuth) AmIAnAuditor() bool { return false }
332+
func (a *EscrowAuth) IsMine(ctx context.Context, tok *token.Token) (string, []string, bool) { ... }
333+
func (a *EscrowAuth) Issued(_ context.Context, _ driver.Identity, _ *token.Token) bool { return false }
334+
func (a *EscrowAuth) OwnerType(raw []byte) (driver.IdentityType, []byte, error) { ... }
335+
```
336+
337+
Register it in **both** driver files inside `NewAuthorizationMultiplexer`:
338+
339+
```go
340+
// token/core/fabtoken/v1/driver/driver.go (and the zkatdlog equivalent)
341+
authorization := common.NewAuthorizationMultiplexer(
342+
common.NewTMSAuthorization(...),
343+
htlc.NewScriptAuth(ws),
344+
multisig.NewEscrowAuth(ws),
345+
boolpolicy.NewEscrowAuth(ws),
346+
mynew.NewEscrowAuth(ws), // ← add here
347+
)
348+
```
349+
350+
#### Step 6 — Add a wallet wrapper
351+
352+
Create an `OwnerWallet` wrapper (see `token/services/ttx/boolpolicy/wallet.go`) that filters the unspent token list to tokens whose owner is the new identity type, and exposes domain-specific helpers (e.g. `VerifyApprover`).
353+
354+
#### Step 7 — Wire the recipient-negotiation protocol
355+
356+
If the new identity requires interactive negotiation between parties to assemble the composite identity before a transfer, add a `RequestMyNewIdentity` function following the pattern of `ttx.RequestPolicyIdentity` (`token/services/ttx/recipients.go`). The function sends a typed request, each counterparty responds with its component data, and the initiator assembles the final composite identity.
357+
358+
#### Step 8 — Add integration views
359+
360+
Create initiator and responder views in the integration layer (e.g. `integration/token/fungible/views/mynew.go`) following the pattern in `boolpolicy.go`:
361+
362+
- **Lock view** — transfers tokens to a recipient with the new composite identity.
363+
- **Spend view** — spends those tokens, optionally with restricted signer sets.
364+
- **Balance view** — queries the policy-owned token balance (modelled on `PolicyOwnedBalanceView`).
365+
- **Responder views** — ACK and endorse spend requests for AND-style policies.
366+
367+
Register all view factories and responders in the integration SDK (`integration/token/fungible/sdk/party/sdk.go`).
368+
369+
#### Step 9 — Add tests
370+
371+
- **Unit tests** for the verifier (`sig_test.go` pattern) and for `EscrowAuth.IsMine` (`auth_test.go` pattern).
372+
- **Integration tests** in `integration/token/fungible/tests.go` + the relevant `dlog_test.go` `Describe` block, following `TestPolicyOR` / `TestPolicyAND`.
373+
374+
#### Summary checklist
375+
376+
| # | What | Where |
377+
|:--|:-----|:------|
378+
| 1 | Reserve type tag | `token/driver/wallet.go` |
379+
| 2 | Wire format + Wrap/Unwrap | `token/services/identity/mynew/` |
380+
| 3 | Verifier + Deserializer | same package; register in both drivers |
381+
| 4 | Signature format + JoinSignatures | same package |
382+
| 5 | EscrowAuth + register in drivers | `token/services/ttx/mynew/auth.go` |
383+
| 6 | OwnerWallet wrapper | `token/services/ttx/mynew/wallet.go` |
384+
| 7 | Recipient-negotiation protocol | `token/services/ttx/recipients.go` |
385+
| 8 | Integration views + SDK registration | `integration/token/fungible/views/mynew.go` |
386+
| 9 | Unit + integration tests | alongside each new file |

docs/services/ttx.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,61 @@ sequenceDiagram
159159
Note over I,R: Full RecipientData on wire today (responder sends local wallet RecipientData)
160160
```
161161

162+
### PolicyIdentity — Boolean-Expression-Governed Ownership
163+
164+
The TTX service supports **PolicyIdentity** owners: tokens whose spending requires satisfying a boolean expression over a set of component identities. This enables richer access-control than simple multisig (M-of-N) — for example, an OR clause where any single co-owner may spend unilaterally, or complex nested expressions.
165+
166+
#### Creating a PolicyIdentity
167+
168+
Call `RequestPolicyIdentity` (in `token/services/ttx/recipients.go`) to negotiate a composite identity from all co-owners before building the transfer:
169+
170+
```go
171+
recipient, err := bptx.RequestRecipientIdentity(ctx, "$0 OR $1",
172+
[]view.Identity{bobFSCIdentity, charlieFSCIdentity},
173+
token.WithTMSIDPointer(tmsID),
174+
)
175+
```
176+
177+
Each co-owner's node responds with its component identity; the SDK assembles the `PolicyIdentity` envelope automatically.
178+
179+
#### Policy Expression Syntax
180+
181+
| Expression | Meaning |
182+
|:-----------|:--------|
183+
| `$0 OR $1` | Either component 0 **or** component 1 can spend alone. |
184+
| `$0 AND $1` | Both component 0 **and** component 1 must sign. |
185+
| `($0 OR $1) AND $2` | One of the first two parties plus party 2 must sign. |
186+
187+
`$N` is a zero-based index into the ordered component identity list supplied when creating the token.
188+
189+
#### Spending — OR Policy
190+
191+
For an OR policy the initiator alone can satisfy the policy. Pass `WithPolicySigners` to restrict signature collection to only the signing party's slot; the remaining slots are left nil (which is valid for OR branches):
192+
193+
```go
194+
_, err = context.RunView(ttx.NewCollectEndorsementsView(tx,
195+
ttx.WithPolicySigners(myComponentIdentity),
196+
))
197+
```
198+
199+
#### Spending — AND Policy
200+
201+
For an AND policy all co-owners must endorse. Use `RequestSpendView` (in `token/services/ttx/boolpolicy/spend.go`) to notify co-owners before assembling the transaction, then collect endorsements from all components without restriction:
202+
203+
```go
204+
_, err = context.RunView(bptx.NewRequestSpendView(unspentToken, serviceOpts...))
205+
// ... build tx ...
206+
_, err = context.RunView(ttx.NewCollectEndorsementsView(tx))
207+
```
208+
209+
Co-owners run `EndorseSpendView` (via `EndorseSpend`) on their side, which ACKs the spend request and then endorses the assembled transaction.
210+
211+
#### Wallet and Authorization
212+
213+
The `boolpolicy.OwnerWallet` (in `token/services/ttx/boolpolicy/wallet.go`) wraps a standard owner wallet and filters the token list to policy-type tokens. `VerifyApprover` can be used to assert that a given identity is one of the named component identities before allowing a spend.
214+
215+
The `EscrowAuth` struct (in `token/services/ttx/boolpolicy/auth.go`) implements the `Authorization` interface: `IsMine` returns true if any component identity of the policy token belongs to one of the node's owner wallets.
216+
162217
## Token Operations
163218

164219
The TTX service supports three primary operations through the `TokenRequest` API:

integration/token/fungible/dlog/dlog_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,16 @@ var _ = Describe("EndToEnd", func() {
115115
It("succeeded", Label("T12"), func() { fungible.TestMultiSig(ts.II, selector) })
116116
})
117117

118+
Describe("PolicyIdentity", t.Label, func() {
119+
ts, selector := newTestSuite(t.CommType, Aries, t.ReplicationFactor, "", "alice", "bob", "charlie")
120+
BeforeEach(ts.Setup)
121+
AfterEach(ts.TearDown)
122+
It("OR and AND succeeded", Label("T14", "T15"), func() {
123+
fungible.TestPolicyOR(ts.II, selector)
124+
fungible.TestPolicyAND(ts.II, selector)
125+
})
126+
})
127+
118128
Describe("Redeem to yourself", t.Label, func() {
119129
ts, selector := newTestSuite(t.CommType, Aries, t.ReplicationFactor, "", "alice", "bob", "charlie")
120130
BeforeEach(ts.Setup)

integration/token/fungible/sdk/party/sdk.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/hyperledger-labs/fabric-smart-client/platform/view/services/view"
1515
views1 "github.com/hyperledger-labs/fabric-token-sdk/integration/token/common/views"
1616
"github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/views"
17+
"github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx/boolpolicy"
1718
"github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx/multisig"
1819
)
1920

@@ -74,9 +75,15 @@ func (p *SDK) Install() error {
7475
registry.RegisterResponder(&views.AcceptCashView{}, &views.TransferView{}),
7576
registry.RegisterResponder(&views.AcceptCashView{}, &views.TransferWithSelectorView{}),
7677
registry.RegisterResponder(&views.AcceptPreparedCashView{}, &views.PrepareTransferView{}),
78+
registry.RegisterFactory("PolicyLock", &views.PolicyLockViewFactory{}),
79+
registry.RegisterFactory("PolicySpend", &views.PolicySpendViewFactory{}),
80+
registry.RegisterFactory("PolicyOwnedBalance", &views.PolicyOwnedBalanceViewFactory{}),
7781
registry.RegisterResponder(&views.AcceptCashView{}, &views.MultiSigLockView{}),
7882
registry.RegisterResponder(&views.AcceptCashView{}, &views.MultiSigSpendView{}),
7983
registry.RegisterResponder(&views.MultiSigAcceptSpendView{}, &multisig.RequestSpendView{}),
84+
registry.RegisterResponder(&views.AcceptCashView{}, &views.PolicyLockView{}),
85+
registry.RegisterResponder(&views.AcceptCashView{}, &views.PolicySpendView{}),
86+
registry.RegisterResponder(&views.PolicyAcceptSpendView{}, &boolpolicy.RequestSpendView{}),
8087
registry.RegisterResponder(&views.SwapResponderView{}, &views.SwapInitiatorView{}),
8188
registry.RegisterResponder(&views.AcceptCashView{}, &views.MaliciousTransferView{}),
8289
)

integration/token/fungible/support.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,26 @@ func CheckBalanceForTMSID(network *integration.Infrastructure, ref *token3.NodeR
301301
}).WithArguments(network, ref, wallet, typ, expected, tmsID).WithTimeout(eventualCheckTimeout).WithPolling(eventualCheckPolling).Should(gomega.Succeed())
302302
}
303303

304+
func CheckPolicyOwnedBalance(network *integration.Infrastructure, ref *token3.NodeReference, wallet string, typ token.Type, expected uint64) {
305+
CheckPolicyOwnedBalanceForTMSID(network, ref, wallet, typ, expected, nil)
306+
}
307+
308+
func CheckPolicyOwnedBalanceForTMSID(network *integration.Infrastructure, ref *token3.NodeReference, wallet string, typ token.Type, expected uint64, tmsID *token2.TMSID) {
309+
res, err := network.Client(ref.ReplicaName()).CallView("PolicyOwnedBalance", common.JSONMarshall(&views.PolicyOwnedBalanceQuery{
310+
Wallet: wallet,
311+
Type: typ,
312+
TMSID: tmsID,
313+
}))
314+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
315+
b := &views.Balance{}
316+
common.JSONUnmarshal(res.([]byte), b)
317+
gomega.Expect(b.Type).To(gomega.BeEquivalentTo(typ))
318+
q, err := token.ToQuantity(b.Quantity, 64)
319+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
320+
expectedQ := token.NewQuantityFromUInt64(expected)
321+
gomega.Expect(expectedQ.Cmp(q)).To(gomega.BeEquivalentTo(0), "[%s]!=[%s]", expected, q)
322+
}
323+
304324
func CheckCoOwnedBalance(network *integration.Infrastructure, ref *token3.NodeReference, wallet string, typ token.Type, expected uint64) {
305325
CheckCoOwnedBalanceForTMSID(network, ref, wallet, typ, expected, nil)
306326
}
@@ -1490,6 +1510,74 @@ func MultiSigSpendCashForTMSID(network *integration.Infrastructure, sender *toke
14901510
return txID
14911511
}
14921512

1513+
// PolicyLockCash locks amount tokens of the given type into a policy identity
1514+
// composed of the given receivers and governed by the boolean policy expression.
1515+
func PolicyLockCash(network *integration.Infrastructure, sender *token3.NodeReference, wallet string, typ token.Type, amount uint64, policy string, receivers []*token3.NodeReference, auditor *token3.NodeReference) string {
1516+
return PolicyLockCashForTMSID(network, sender, wallet, typ, amount, policy, receivers, auditor, nil)
1517+
}
1518+
1519+
// PolicyLockCashForTMSID is like PolicyLockCash but pins a specific TMSID.
1520+
func PolicyLockCashForTMSID(network *integration.Infrastructure, sender *token3.NodeReference, wallet string, typ token.Type, amount uint64, policy string, receivers []*token3.NodeReference, auditor *token3.NodeReference, tmsID *token2.TMSID) string {
1521+
parties := make([]view.Identity, len(receivers))
1522+
for i, r := range receivers {
1523+
parties[i] = network.Identity(r.Id())
1524+
}
1525+
txidBoxed, err := network.Client(sender.ReplicaName()).CallView("PolicyLock", common.JSONMarshall(&views.PolicyLock{
1526+
Auditor: auditor.Id(),
1527+
Wallet: wallet,
1528+
Type: typ,
1529+
Amount: amount,
1530+
Policy: policy,
1531+
PolicyParties: parties,
1532+
TMSID: tmsID,
1533+
}))
1534+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
1535+
1536+
return common.JSONUnmarshalString(txidBoxed)
1537+
}
1538+
1539+
// PolicySpendCashOR spends a policy token using only the sender as the signing
1540+
// component identity (OR-policy optimisation: no co-owner coordination needed).
1541+
func PolicySpendCashOR(network *integration.Infrastructure, sender *token3.NodeReference, wallet string, typ token.Type, receiver *token3.NodeReference, auditor *token3.NodeReference) string {
1542+
return PolicySpendCashORForTMSID(network, sender, wallet, typ, receiver, auditor, nil)
1543+
}
1544+
1545+
// PolicySpendCashORForTMSID is like PolicySpendCashOR but pins a specific TMSID.
1546+
func PolicySpendCashORForTMSID(network *integration.Infrastructure, sender *token3.NodeReference, wallet string, typ token.Type, receiver *token3.NodeReference, auditor *token3.NodeReference, tmsID *token2.TMSID) string {
1547+
txidBoxed, err := network.Client(sender.ReplicaName()).CallView("PolicySpend", common.JSONMarshall(&views.PolicySpend{
1548+
Auditor: auditor.Id(),
1549+
Wallet: wallet,
1550+
TMSID: tmsID,
1551+
Recipient: network.Identity(receiver.Id()),
1552+
TokenType: typ,
1553+
Signers: []view.Identity{network.Identity(sender.Id())},
1554+
}))
1555+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
1556+
1557+
return common.JSONUnmarshalString(txidBoxed)
1558+
}
1559+
1560+
// PolicySpendCashAND spends a policy token that requires all co-owners to sign,
1561+
// coordinating with them via RequestSpendView before collecting endorsements.
1562+
func PolicySpendCashAND(network *integration.Infrastructure, sender *token3.NodeReference, wallet string, typ token.Type, receiver *token3.NodeReference, auditor *token3.NodeReference) string {
1563+
return PolicySpendCashANDForTMSID(network, sender, wallet, typ, receiver, auditor, nil)
1564+
}
1565+
1566+
// PolicySpendCashANDForTMSID is like PolicySpendCashAND but pins a specific TMSID.
1567+
func PolicySpendCashANDForTMSID(network *integration.Infrastructure, sender *token3.NodeReference, wallet string, typ token.Type, receiver *token3.NodeReference, auditor *token3.NodeReference, tmsID *token2.TMSID) string {
1568+
txidBoxed, err := network.Client(sender.ReplicaName()).CallView("PolicySpend", common.JSONMarshall(&views.PolicySpend{
1569+
Auditor: auditor.Id(),
1570+
Wallet: wallet,
1571+
TMSID: tmsID,
1572+
Recipient: network.Identity(receiver.Id()),
1573+
TokenType: typ,
1574+
// Signers is nil: all co-owners are contacted and must sign.
1575+
}))
1576+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
1577+
1578+
return common.JSONUnmarshalString(txidBoxed)
1579+
}
1580+
14931581
func BindIssuerNetworkAndSigningIdentities(network *integration.Infrastructure, issuer *token3.NodeReference, issuerPublicKey []byte, onNodes ...*token3.NodeReference) {
14941582
for _, node := range onNodes {
14951583
for _, nodeReplica := range node.AllNames() {

0 commit comments

Comments
 (0)