hyperledger / hyperledger/fabric-x-sdk
Live MSP configuration from the ledger
- Dominant language
- Go
- Stars
- 3
- Forks
- 5
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 9
Description
## Context
This builds on [hyperledger/fabric-x-sdk#41](https://github.com/hyperledger/fabric-x-sdk/issues/41)
("Cryptographic verification for endorsement proposals"), which introduces `identity.TrustStore` — a multi-org MSP trust store, currently loadable only from
static local folders (`identity.NewTrustStore(orgs map[string]string)`). That's fine for a fixed
set of orgs known ahead of time, but org/MSP trust on a real Fabric-X channel is not actually
static: orgs can be added, and CAs rotated, at runtime via ordinary channel config-update
transactions, and nothing in the SDK today lets a client observe that.
Research into `fabric-x-committer` (the service this SDK's `network/fabricx` package talks to)
confirms it reuses classic Hyperledger Fabric's channel-config machinery close to verbatim:
- Org/MSP definitions are **not** static per-deployment config. A `common.HeaderType_CONFIG`
envelope submitted through the ordering service is validated and converted into a write to a
reserved `_config` namespace on the ledger's state DB (`service/sidecar/mapping.go`,
`service/verifier/policy/policy.go` in the committer source). The verifier rebuilds its
`channelconfig.Bundle`/`MSPManager` live from this on every config update — no committer restart,
confirmed down to the specific functions (`service/verifier/verify.go` `updatePolicies`).
- This is already queryable today, via an RPC the SDK has never used:
`committerpb.QueryServiceClient.GetConfigTransaction(Empty) → applicationpb.ConfigTransaction{envelope, version}`.
`network/fabricx/peer.go` currently only wires up `BlockQueryServiceClient` and `NotifierClient` —
`QueryServiceClient` is unused in this SDK entirely.
- `fabric-x-common/common/channelconfig` ports classic Fabric's config-bundle machinery
essentially unchanged: `channelconfig.NewBundleFromEnvelope(env *common.Envelope, bccsp bccsp.BCCSP) (*Bundle, error)`
parses a config envelope into a `*Bundle` whose `.MSPManager()` is a ready `msp.MSPManager` —
confirmed working end-to-end with only a `bccsp.BCCSP` (no running peer, no ledger access) by
`common/channelconfig/realconfig_test.go`'s `TestWithRealConfigTX`.
- There is no server-side "unwrap and hand back per-org MSP config" convenience endpoint — a client
gets the opaque envelope bytes from `GetConfigTransaction` and must parse them itself with the
same `channelconfig` machinery the committer uses internally. That parsing step is exactly what
`identity.TrustStore` already exists to wrap.
- A real genesis/config block already sits in this repo's own `testdata/crypto/config-block.pb.bin`
(produced by `make init-x`), decoded and confirmed to contain `Application.groups.peer-org-0`
/`peer-org-1`, each with a `values["MSP"]` holding a standard `msp.FabricMSPConfig` — i.e. exactly
the shape this feature needs to parse.
The result: a client can fetch the channel's current trust configuration directly from the ledger
via a call the SDK already has a connection for, instead of requiring every org's MSP folder to be
pre-provisioned on disk — and can detect when it's changed (`ConfigTransaction.Version`) and rebuild.
## Approach
### 1. `identity.NewTrustStoreFromConfig` — new constructor in `identity/truststore.go`
A second constructor alongside the folder-based `NewTrustStore`, taking an already-unmarshaled
config envelope instead of a set of directories:
```go
// NewTrustStoreFromConfig builds a trust store from a channel config transaction envelope — the
// live, ledger-sourced counterpart to NewTrustStore's static MSP folders. envelope is a
// HeaderType_CONFIG envelope, e.g. from network/fabricx.Peer.ConfigTransaction or extracted from a
// config block.
func NewTrustStoreFromConfig(envelope *common.Envelope) (TrustStore, error) {
bundle, err := channelconfig.NewBundleFromEnvelope(envelope, factory.GetDefault())
if err != nil {
return TrustStore{}, fmt.Errorf("parse channel config: %w", err)
}
return TrustStore{mgr: bundle.MSPManager()}, nil
}
```
New imports: `github.com/hyperledger/fabric-x-common/common/channelconfig`,
`github.com/hyperledger/fabric-lib-go/bccsp/factory`, `github.com/hyperledger/fabric-protos-go-apiv2/common`.
No change to `TrustStore`'s existing fields/methods — both constructors produce the same shape, so
`endorsement.Verifier` (from #41) needs zero changes to accept a live-sourced store.
### 2. `network/fabricx.Peer.ConfigTransaction` — new method in `network/fabricx/peer.go`
**Why not classic Fabric's client-side config-fetch mechanisms.** Classic Fabric gives a pure
client two ways to get the current config without running a full peer: invoking the `cscc` system
chaincode's `GetChannelConfig` through a normal endorsement proposal, or a `Deliver`-based fetch of
the latest block followed by reading its `LAST_CONFIG` metadata index and re-seeking that specific
block number. Neither is the right fit here:
- `cscc` is a system *chaincode*, invoked like any other chaincode through the classic
`ProcessProposal`/endorsement path. Fabric-X has no chaincode layer at all (per this SDK's own
README: "Fabric-X does not support 'traditional' chaincode") — there is no `cscc` to call.
- The `Deliver`+`LAST_CONFIG`-index approach *would* work against Fabric-X too (the underlying
block/metadata format is unchanged, and `network.Peer.SubscribeBlocks` already speaks this
protocol) — it's a real, viable alternative, not a dead end. It's just more machinery than
necessary here: it needs a block-seek round trip (seek newest, read metadata, re-seek by number)
to reconstruct something `GetConfigTransaction` already hands back directly, in one call, with an
explicit `version` for staleness checks that the block-metadata approach doesn't give you for
free. The block-streaming path is deliberately left as the out-of-scope "react to config changes
in real time" follow-up below, where its properties (already-open stream, no extra RPC) are
actually the point — it's the wrong tool for a one-shot "what's the config right now" fetch.
Mirrors the existing `BlockHeight` method exactly in style:
```go
// ConfigTransaction fetches the channel's current config transaction envelope and version from
// the committer's QueryService. Feed the envelope into identity.NewTrustStoreFromConfig to build a
// TrustStore reflecting the ledger's current set of trusted orgs.
func (p *Peer) ConfigTransaction(ctx context.Context) (envelope *common.Envelope, version uint64, err error) {
client := committerpb.NewQueryServiceClient(p.Connection())
resp, err := client.GetConfigTransaction(ctx, &emptypb.Empty{})
if err != nil {
return nil, 0, fmt.Errorf("get config transaction: %w", err)
}
env, err := protoutil.UnmarshalEnvelope(resp.Envelope)
if err != nil {
return nil, 0, fmt.Errorf("unmarshal config envelope: %w", err)
}
return env, resp.Version, nil
}
```
New imports: `github.com/hyperledger/fabric-x-common/protoutil`,
`github.com/hyperledger/fabric-protos-go-apiv2/common` (`committerpb`/`emptypb` are already imported
in this file for `BlockHeight`).
### 3. Usage pattern (doc comment / README note, no new abstraction)
```go
env, version, err := peer.ConfigTransaction(ctx)
trust, err := identity.NewTrustStoreFromConfig(env)
verifier, err := endorsement.NewVerifier(trust, "OR('peer-org-0.member', 'peer-org-1.member')")
```
`version` lets a long-running caller (e.g. an endorser) decide when to re-fetch and rebuild — poll
on a ticker and skip rebuilding when the version is unchanged. This plan does not add a built-in
polling/watcher abstraction; that's better scoped as its own follow-up once there's a concrete
consumer, consistent with how `network.Synchronizer` already leaves "what to do with new blocks" to
the caller rather than prescribing a policy.
### 4. Tests
`identity/truststore_test.go` (already being added by #41) gets a second set of cases
for `NewTrustStoreFromConfig`, built from a synthetic genesis/config block rather than requiring
Docker: `fabric-x-common/common/configtx/test` already provides genesis-block-building test helpers
(`MakeGenesisBlock`/`MakeChannelConfig`, built on `configtxgen.NewChannelGroup` + a real
`genesis.NewFactoryImpl`) used by fabric-x-common's own tests — reuse that to build a small
multi-org config block in-process, extract its envelope with `protoutil.ExtractEnvelope(block, 0)`,
and feed it through `NewTrustStoreFromConfig`. This keeps the test hermetic (no `make init-x`, no
Docker) while still exercising the real parsing path. Cases: valid multi-org config produces a
TrustStore that verifies identities from each org (using the same `internal/msptest` fixture
signer helper from #41, or the org identities the genesis-block helper itself
provisions); malformed/non-CONFIG envelope is rejected with a clear error.
`network/fabricx/peer_test.go`: `ConfigTransaction` itself (the gRPC call) is only meaningfully
testable against a real committer — cover it in the existing `TestFabricXCommitter` integration
test (gated behind `make start-x`, already skipped in `-short` mode), not as a new unit test.
## Out of scope (explicitly, for a follow-up)
- **Classic Fabric support.** This SDK supports both classic Fabric and Fabric-X, generally via
parallel `fabric`/`fabricx` subpackages (`network/fabric` + `network/fabricx`,
`endorsement/fabric` + `endorsement/fabricx`, `blocks/fabric` + `blocks/fabricx`). This issue only
adds `network/fabricx.Peer.ConfigTransaction` — there is no `network/fabric.Peer` equivalent here.
A classic-Fabric fetch would need its own mechanism (most likely the Deliver+`LAST_CONFIG`-index
approach described above, since there's no Fabric-X-style `QueryService` on a classic peer), but
`identity.NewTrustStoreFromConfig` itself needs no changes to support it — it already takes a
protocol-agnostic `*common.Envelope`, regardless of how that envelope was fetched. Natural
follow-up once there's a concrete classic-Fabric consumer.
- A built-in polling/watcher helper that automatically rebuilds a `Verifier`/`TrustStore` when the
config version changes.
- Recognizing `HeaderType_CONFIG` blocks as they stream through `network.Synchronizer`/
`blocks.Processor` (today they're silently parsed into empty blocks and dropped — confirmed in
`blocks/fabric/parser.go` and `blocks/fabricx/parser.go`, both with a `// skip config
transactions` comment). That would let a client already running a Synchronizer react to config
changes in real time with no extra RPCs, as a complement to the poll-based `ConfigTransaction`
call added here — worth its own issue once there's a concrete need, since it touches the shared
`BlockHandler`/`BlockProcessor` interfaces used by both Fabric and Fabric-X parsers.
- `QueryService.GetNamespacePolicies` (the sibling RPC for namespace endorsement policies rather
than org/MSP trust) — same client-wiring pattern, but a distinct concept from this issue's scope.
## Verification
- `go build ./...` / `go vet ./...` — no new `go.mod` changes expected (`channelconfig`,
`common/configtx/test`, `protoutil` are all sub-packages of the already-vendored `fabric-x-common`).
- `go test ./identity/... -race -v` — new `NewTrustStoreFromConfig` cases pass, including the
rejection cases.
- `make init-x && make start-x && make test-x` — manually confirm `peer.ConfigTransaction(ctx)`
against a real committer returns a parseable envelope whose `identity.NewTrustStoreFromConfig(...)`
successfully verifies a signature from `testdata/crypto/peerOrganizations/peer-org-0.com/...`
(real crypto material already produced by this target).
- `make checks`.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with identity/truststore.go and network/fabricx/peer.go, comparing the existing folder-based constructor and BlockHeight RPC wiring. Add focused cases in identity/truststore_test.go and cover the live RPC through the existing TestFabricXCommitter integration test. Done means config envelopes are fetched, parsed, rejected when malformed, and usable for multi-org identity verification.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api, backend, security
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100