Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions api/committerpb/snapshot.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions api/committerpb/snapshot.proto
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ message SnapshotState {
COMPLETED = 3;
FAILED = 4;
CHECKPOINTED = 5;
ABORTED = 6;
}

// Reference identifying snapshot transaction and its position in the block.
Expand Down
64 changes: 64 additions & 0 deletions api/committerpb/snapshot_abort_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package committerpb

import (
"bytes"
"fmt"

"github.com/hyperledger/fabric-x-common/common/ledger/util"
)

// snapshotAbortKeyPrefix distinguishes an abort key from a snapshot record's key. Both
// live in the `_snapshot` namespace table, and a record's key is a transaction ID, so
// an abort must be recognizable from the key alone: that is what lets the commit path
// tell an abort transaction from a snapshot request without reading the record it
// names. The sidecar requires transaction IDs to be valid UTF-8. The leading 0xff byte
// cannot occur in valid UTF-8, so an abort key cannot collide with a transaction ID.
var snapshotAbortKeyPrefix = []byte("\xffabort/")

// SnapshotAbortKey returns the `_snapshot` namespace key an abort snapshot transaction
// writes to abandon the snapshot taken at blockNum.
//
// The snapshot is named by block number alone -- a snapshot is a cut of committed state and
// a block holds at most one `_snapshot` transaction, so the transaction number adds no
// distinguishing power -- and the number is encoded order-preserving so aborts can be
// compared and range-scanned directly on the stored key.
func SnapshotAbortKey(blockNum uint64) []byte {
return append(bytes.Clone(snapshotAbortKeyPrefix), util.EncodeOrderPreservingVarUint64(blockNum)...)
}

// IsSnapshotAbortKey reports whether key is an abort key rather than a snapshot
// record's key.
//
// Only the prefix is inspected: a prefixed key whose block number does not decode is an
// abort transaction with a bad key, which must be rejected as one rather than mistaken
// for a snapshot request.
func IsSnapshotAbortKey(key []byte) bool {
return bytes.HasPrefix(key, snapshotAbortKeyPrefix)
}

// BlockNumFromSnapshotAbortKey decodes an abort key into the block number of the
// snapshot it abandons.
//
// Trailing bytes are rejected rather than ignored, as for a checkpoint key: accepting a
// prefix would attribute the abort to the wrong snapshot.
func BlockNumFromSnapshotAbortKey(key []byte) (uint64, error) {
if !IsSnapshotAbortKey(key) {
return 0, fmt.Errorf("snapshot abort key [%v] does not start with %q", key, snapshotAbortKeyPrefix)
}
encoded := key[len(snapshotAbortKeyPrefix):]
blockNum, n, err := util.DecodeOrderPreservingVarUint64(encoded)
if err != nil {
return 0, fmt.Errorf("failed to decode block number from snapshot abort key [%v]: %w", key, err)
}
if n != len(encoded) {
return 0, fmt.Errorf("snapshot abort key [%v] has %d trailing bytes after the block number",
key, len(encoded)-n)
}
return blockNum, nil
}
102 changes: 102 additions & 0 deletions api/committerpb/snapshot_abort_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package committerpb_test

import (
"testing"
"unicode/utf8"

"github.com/stretchr/testify/require"

"github.com/hyperledger/fabric-x-common/api/committerpb"
)

func TestSnapshotAbortKeyRoundTrip(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
blockNum uint64
}{
{name: "zero", blockNum: 0},
{name: "one", blockNum: 1},
{name: "small", blockNum: 42},
{name: "large", blockNum: 1 << 40},
{name: "max", blockNum: ^uint64(0)},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
key := committerpb.SnapshotAbortKey(tc.blockNum)
require.False(t, utf8.Valid(key), "abort keys must not be valid UTF-8 transaction IDs")
require.True(t, committerpb.IsSnapshotAbortKey(key))

decoded, err := committerpb.BlockNumFromSnapshotAbortKey(key)
require.NoError(t, err)
require.Equal(t, tc.blockNum, decoded)
})
}
}

// An abort key must never be mistaken for a snapshot record's key, which is a
// transaction ID, because both live in the ns__snapshot table.
func TestIsSnapshotAbortKeyRejectsRecordKeys(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
key []byte
}{
{name: "transaction ID", key: []byte("3f2a1b9c8d7e6f504132231415161718")},
{name: "transaction ID with abort prefix", key: []byte("abort/foo")},
{name: "transaction ID matching old abort key", key: []byte("abort/\x01\x07")},
{name: "unicode transaction ID", key: []byte("快照/7")},
{name: "empty", key: nil},
{name: "bare block number without the prefix", key: []byte{1, 7}},
{name: "prefix in the middle", key: []byte("xabort/")},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
require.False(t, committerpb.IsSnapshotAbortKey(tc.key))

_, err := committerpb.BlockNumFromSnapshotAbortKey(tc.key)
require.ErrorContains(t, err, "does not start with")
})
}
}

func TestBlockNumFromSnapshotAbortKeyRejectsMalformedKeys(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
key []byte
errContains string
}{
{
name: "prefix with no block number",
key: []byte("\xffabort/"),
errContains: "failed to decode block number",
},
{
name: "trailing bytes after the block number",
key: append(committerpb.SnapshotAbortKey(7), []byte("junk")...),
errContains: "trailing bytes",
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := committerpb.BlockNumFromSnapshotAbortKey(tc.key)
require.ErrorContains(t, err, tc.errContains)
})
}
}

// The encoding is order-preserving so an administrator can range-scan aborts by block
// number directly on the stored key, exactly as for a checkpoint key.
func TestSnapshotAbortKeyIsOrderPreserving(t *testing.T) {
t.Parallel()
for i := range uint64(1_000) {
require.Greater(t, committerpb.SnapshotAbortKey(i+1), committerpb.SnapshotAbortKey(i))
}
}
14 changes: 11 additions & 3 deletions api/committerpb/status.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion api/committerpb/status.proto
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ enum Status {
MALFORMED_MISSING_SIGNATURE = 113; // Number of signatures does not match the number of namespaces.
MALFORMED_NAMESPACE_POLICY_INVALID = 114; // Invalid namespace policy.
MALFORMED_CONFIG_TX_INVALID = 115; // Invalid configuration transaction.
MALFORMED_SNAPSHOT_NOT_MARKER_ONLY = 116; // _snapshot TX has a non-empty read-write set; only a marker-only _snapshot (entirely empty read-write set) is valid.
MALFORMED_SNAPSHOT_NOT_MARKER_ONLY = 116; // _snapshot TX is neither a marker-only snapshot request (entirely empty read-write set) nor an abort snapshot TX (exactly one read-write, no reads, no blind writes).
MALFORMED_CHECKPOINT_INVALID_KEY = 117; // _checkpoint TX read-write key does not decode as a valid TxHeight.
MALFORMED_SYSTEM_TX_NOT_STANDALONE = 118; // System TX must be standalone and cannot be mixed with other namespaces.
REJECTED_SNAPSHOT_IN_PROGRESS = 119; // Another snapshot is still being hashed / awaiting its checkpoint (a prior _snapshot row is not yet CHECKPOINTED).
REJECTED_SNAPSHOT_NO_CHECKPOINT = 120; // A prior snapshot exists that was never checkpointed, so a new snapshot cannot be accepted.
REJECTED_DUPLICATE_SNAPSHOT_IN_BLOCK = 121; // More than one snapshot TX appeared in the same block; only the first is processed and the rest are rejected regardless of the first's outcome.
MALFORMED_SNAPSHOT_INVALID_ABORT_KEY = 122; // An abort snapshot TX's read-write key does not decode as an abort key naming a snapshot's block number.
REJECTED_SNAPSHOT_ABORT_NO_SUCH_SNAPSHOT = 123; // An abort snapshot TX names a block that is not the snapshot awaiting its checkpoint, or that snapshot's lifecycle is already closed (CHECKPOINTED/ABORTED).
}
Loading