Skip to content

Commit 507c27a

Browse files
committed
feat: decode blob subscribers and subscription ID
1 parent 859a6f1 commit 507c27a

File tree

6 files changed

+200
-44
lines changed

6 files changed

+200
-44
lines changed

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -959,13 +959,13 @@ This will emit a `DeleteBlob` event and delete the blob from the network.
959959
##### Get a blob
960960

961961
```sh
962-
cast abi-decode "getBlob(string)((uint64,string,bytes,uint8))" $(cast call --rpc-url $EVM_RPC_URL $BLOBS "getBlob(string)" "rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq")
962+
cast abi-decode "getBlob(string)((uint64,string,(address,(string,bytes)[])[],uint8))" $(cast call --rpc-url $EVM_RPC_URL $BLOBS "getBlob(string)" "rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq")
963963
```
964964

965965
This will return the following response:
966966

967967
```sh
968-
(6, "utiakbxaag7udhsriu6dm64cgr7bk4zahiudaaiwuk6rfv43r3rq", 0xa156040a..., 1)
968+
(6, "utiakbxaag7udhsriu6dm64cgr7bk4zahiudaaiwuk6rfv43r3rq", [(0x90F79bf6EB2c4f870365E785982E1f101E93b906, [("foo", 0x861904...)])], 1)
969969
```
970970

971971
Which maps to the `Blob` struct:
@@ -974,9 +974,19 @@ Which maps to the `Blob` struct:
974974
struct Blob {
975975
uint64 size; // 6
976976
string metadataHash; // utiakbxaag7udhsriu6dm64cgr7bk4zahiudaaiwuk6rfv43r3rq
977-
bytes subscribers; // 0xa156040a... (raw bytes of the subscribers map)
977+
Subscribers subscribers; // See `Subscribers` struct below
978978
BlobStatus status; // 1 (Resolved)
979979
}
980+
981+
struct Subscribers {
982+
address subscriber; // 0x90F79bf6EB2c4f870365E785982E1f101E93b906
983+
SubscriptionGroup[] subscriptionGroup; // See `SubscriptionGroup` struct below
984+
}
985+
986+
struct SubscriptionGroup {
987+
string subscriptionId; // "foo"
988+
bytes subscription; // 0x861904...
989+
}
980990
```
981991

982992
##### Get blob status

src/types/BlobTypes.sol

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ struct Blob {
139139
uint64 size;
140140
string metadataHash;
141141
// TODO: decode the following from Rust type: HashMap<Address, SubscriptionGroup>
142-
bytes subscribers;
142+
Subscriber[] subscribers;
143143
BlobStatus status;
144144
}
145145

@@ -153,20 +153,56 @@ enum BlobStatus {
153153
Failed
154154
}
155155

156+
/// @dev A subscriber and their subscription groups.
157+
/// @param subscriber (address): The subscriber address.
158+
/// @param subscriptionGroup (SubscriptionGroup[]): The subscription groups. See {SubscriptionGroup} for more details.
159+
struct Subscriber {
160+
address subscriber;
161+
SubscriptionGroup[] subscriptionGroup;
162+
}
163+
156164
/// @dev Pending subscription information.
157165
/// @param subscriber (address): The subscriber address.
158166
/// @param subscriptionId (string): The subscription ID.
159167
/// @param publicKey (bytes): The public key.
160-
struct Subscription {
161-
address subscriber;
168+
struct SubscriptionGroup {
169+
// TODO: the blobs solidity logic assumes a string key. But, a blob added when pushing to a bucket will serialize
170+
// the key as the blake3(Vec<bucket_address + object_key>). We should probably make this value a bytes type, but all
171+
// of the encoding/decoding logic works...except you might see odd decoding with a bucket-backed blob, like a
172+
// subscription ID of `��0������䣱p�V�%���?��:\u{8}4T�~��V`.
162173
string subscriptionId;
163-
bytes publicKey;
174+
bytes subscription; // TODO: update type
175+
}
176+
177+
/// @dev A subscription to a blob.
178+
/// @param added (uint64): The block number when the subscription was added.
179+
/// @param expiry (uint64): The block number when the subscription will expire.
180+
/// @param autoRenew (bool): Whether the subscription will automatically renew.
181+
/// @param source (string): The source Iroh node ID used for ingestion.
182+
/// @param delegate (Delegate): The delegate origin and caller that may have created the subscription via a credit
183+
/// approval.
184+
/// @param failed (bool): Whether the subscription failed due to an issue resolving the target blob.
185+
struct Subscription {
186+
uint64 added;
187+
uint64 expiry;
188+
bool autoRenew;
189+
string source;
190+
Delegate delegate;
191+
bool failed;
192+
}
193+
194+
/// @dev The delegate origin and caller that may have created the subscription via a credit approval.
195+
/// @param origin (address): The delegate origin.
196+
/// @param caller (address): The caller address.
197+
struct Delegate {
198+
address origin;
199+
address caller;
164200
}
165201

166202
/// @dev Pending blob information. Represents a Rust `(Hash, HashSet<(Address, SubscriptionId, PublicKey)>)`
167-
/// @param hash (bytes): The blob hash.
168-
/// @param subscriptions (Subscription[]): The pending subscriptions.
203+
/// @param blobHash (bytes): The blob hash.
204+
/// @param sourceInfo (Subscription[]): The pending subscriptions.
169205
struct PendingBlob {
170-
bytes hash;
171-
Subscription[] subscriptions;
206+
bytes blobHash;
207+
bytes sourceInfo;
172208
}

src/util/LibBlob.sol

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import {
1212
CreditApproval,
1313
CreditStats,
1414
StorageStats,
15-
SubnetStats
15+
SubnetStats,
16+
Subscriber,
17+
SubscriptionGroup
1618
} from "../types/BlobTypes.sol";
1719
import {KeyValue} from "../types/CommonTypes.sol";
1820
import {InvalidValue, LibWasm} from "./LibWasm.sol";
@@ -116,12 +118,48 @@ library LibBlob {
116118
}
117119
}
118120

121+
/// @dev Decode a subscription ID from CBOR.
122+
/// @param data The encoded subscription ID.
123+
/// @return decoded The decoded subscription ID.
124+
function decodeSubscriptionId(bytes memory data) internal view returns (string memory) {
125+
// If the leading indicator is `a1`, it's a mapping with a single key-value pair; else, default
126+
if (data[0] == hex"a1") {
127+
// Decode the mapping with Key and subscription ID bytes (a single key-value pair)
128+
bytes[2][] memory decoded = data.decodeCborMappingToBytes();
129+
// Second value is the subscription ID bytes (ignore the first value `Key` key)
130+
bytes memory subscriptionId = decoded[0][1].decodeCborBytesArrayToBytes();
131+
return string(subscriptionId);
132+
} else {
133+
return "Default";
134+
}
135+
}
136+
137+
/// @dev Decode subscribers from CBOR.
138+
/// @param data The encoded CBOR mapping of subscribers.
139+
/// @return subscribers The decoded subscribers.
140+
function decodeSubscribers(bytes memory data) internal view returns (Subscriber[] memory subscribers) {
141+
bytes[2][] memory decoded = data.decodeCborMappingToBytes();
142+
subscribers = new Subscriber[](decoded.length);
143+
for (uint256 i = 0; i < decoded.length; i++) {
144+
subscribers[i].subscriber = decoded[i][0].decodeCborAddress();
145+
bytes[2][] memory subscriptionGroupBytes = decoded[i][1].decodeCborMappingToBytes();
146+
subscribers[i].subscriptionGroup = new SubscriptionGroup[](subscriptionGroupBytes.length);
147+
for (uint256 j = 0; j < subscriptionGroupBytes.length; j++) {
148+
subscribers[i].subscriptionGroup[j].subscriptionId = decodeSubscriptionId(subscriptionGroupBytes[j][0]);
149+
subscribers[i].subscriptionGroup[j].subscription = subscriptionGroupBytes[j][1];
150+
}
151+
}
152+
}
153+
154+
/// @dev Decode a blob from CBOR.
155+
/// @param data The encoded CBOR array of a blob.
156+
/// @return blob The decoded blob.
119157
function decodeBlob(bytes memory data) internal view returns (Blob memory blob) {
120158
bytes[] memory decoded = data.decodeCborArrayToBytes();
121159
if (decoded.length == 0) return blob;
122160
blob.size = decoded[0].decodeCborBytesToUint64();
123161
blob.metadataHash = string(decoded[1].decodeBlobHash());
124-
blob.subscribers = decoded[2];
162+
blob.subscribers = decodeSubscribers(decoded[2]);
125163
blob.status = decodeBlobStatus(decoded[3]);
126164
}
127165

@@ -368,6 +406,10 @@ library LibBlob {
368406
return LibWasm.writeToWasmActor(ACTOR_ID, METHOD_ADD_BLOB, _params);
369407
}
370408

409+
/// @dev Delete a blob from the subnet.
410+
/// @param subscriber The address of the subscriber.
411+
/// @param blobHash The hash of the blob.
412+
/// @param subscriptionId The subscription ID.
371413
function deleteBlob(address subscriber, string memory blobHash, string memory subscriptionId) external {
372414
bytes[] memory encoded = new bytes[](3);
373415
encoded[0] = subscriber == address(0) ? LibWasm.encodeCborNull() : subscriber.encodeCborAddress();

src/util/LibWasm.sol

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,32 @@ library LibWasm {
3333
return CBORDecoding.decodeArray(data);
3434
}
3535

36-
/// @dev Decode CBOR encoded bytes array to underlying bytes string (e.g., a serialized Rust `Vec<u8>`).
36+
/// @dev Decode CBOR encoded bytes array to underlying bytes string (e.g., a serialized Rust `Vec<u8>`). Handles up
37+
/// to 255 bytes.
3738
/// @param data The encoded CBOR bytes (e.g., 0x8618681865...).
3839
/// @return result The decoded bytes.
3940
function decodeCborBytesArrayToBytes(bytes memory data) internal pure returns (bytes memory) {
40-
if (uint8(data[0]) < 0x80 || uint8(data[0]) > 0x97) revert InvalidValue("Invalid fixed array indicator");
41-
// Initial data is a CBOR array (0x8<length>) of single byte values (0x18<byte>18<byte>)
42-
uint8 length = uint8(data[0]) - 0x80;
41+
if (uint8(data[0]) < 0x80 || uint8(data[0]) > 0x98) {
42+
revert InvalidValue("Invalid fixed array indicator or length exceeds max size of 255");
43+
}
44+
// Initial data is a CBOR array (0x<indicator>(<length>)) of single byte values (0x18<byte>18<byte>)
45+
uint8 length = data[0] > 0x97 ? uint8(data[1]) : uint8(data[0]) - 0x80;
4346
bytes memory result = new bytes(length);
44-
// Skip first byte (indicator + length) and the second byte (first instance of 0x18 indicator)
45-
// Then, get every other byte since this is the raw value (skipping the 0x18 indicator)
46-
for (uint8 i = 0; i < length; i++) {
47-
result[i] = data[2 * i + 2];
47+
48+
// Track if we skip the only first byte (indicator + length), or also the second byte (if > 23 length)
49+
// Anything greater than 23 length is a fixed array with 0x98 indicator (i.e., max length of 255)
50+
uint16 resultIndex = data[0] > 0x97 ? 2 : 1;
51+
// Skip first and, possibly, second byte (indicator + length); then, every other byte is the raw value, with a
52+
// 0x18 indicator preceding it (i.e., `0x<array_indicator>18<byte>18<byte>`)
53+
for (uint16 i = 0; i < length; i++) {
54+
if (data[resultIndex] == 0x18) {
55+
result[i] = data[resultIndex + 1];
56+
resultIndex += 2;
57+
} else {
58+
// Assume that if the value has no byte prefix (i.e., 0x18), it's the direct value (0x00..0x17)
59+
result[i] = data[resultIndex];
60+
resultIndex += 1;
61+
}
4862
}
4963
return result;
5064
}
@@ -56,15 +70,15 @@ library LibWasm {
5670
if (data[0] != 0x98) revert InvalidValue("Invalid fixed array indicator");
5771
uint8 length = uint8(data[1]);
5872
bytes memory result = new bytes(length);
59-
uint256 index = 2; // Start at the third byte (i.e., the first instance of 0x18 or 0x00 to 0x17)
60-
for (uint8 i = 0; i < length; i++) {
61-
if (data[index] == 0x18) {
62-
result[i] = data[index + 1];
63-
index += 2;
73+
uint16 resultIndex = 2; // Start at the third byte (i.e., the first instance of 0x18 or 0x00 to 0x17)
74+
for (uint16 i = 0; i < length; i++) {
75+
if (data[resultIndex] == 0x18) {
76+
result[i] = data[resultIndex + 1];
77+
resultIndex += 2;
6478
} else {
65-
// Assume that if the value is not a single byte (i.e., 0x18), it's the direct value (0x00..0x17)
66-
result[i] = data[index];
67-
index += 1;
79+
// Assume that if the value has no byte prefix (i.e., 0x18), it's the direct value (0x00..0x17)
80+
result[i] = data[resultIndex];
81+
resultIndex += 1;
6882
}
6983
}
7084
return result;
@@ -81,7 +95,7 @@ library LibWasm {
8195
/// @dev Decode a CBOR encoded string to bytes.
8296
/// @param data The encoded CBOR string.
8397
/// @return decoded The decoded CBOR string.
84-
function decodeStringToBytes(bytes memory data) internal view returns (bytes memory) {
98+
function decodeCborStringToBytes(bytes memory data) internal view returns (bytes memory) {
8599
return CBORDecoding.decodePrimitive(data);
86100
}
87101

@@ -480,7 +494,7 @@ library LibWasm {
480494
uint8 length = uint8(value.length);
481495
bytes memory result;
482496

483-
// Track if we skip the only first byte (indicator + length), or also the second byte (length if > 23 length)
497+
// Track if we skip the only first byte (indicator + length), or also the second byte (if > 23 length)
484498
uint16 resultIndex = 1;
485499
// Anything greater than 23 length is a fixed array with 0x98 indicator (i.e., max length of 255)
486500
if (length > 0x17) {
@@ -496,9 +510,14 @@ library LibWasm {
496510
// 0x18 indicator preceding it (i.e., `0x<array_indicator>18<byte>18<byte>`)
497511
for (uint8 i = 0; i < length; i++) {
498512
uint8 val = uint8(value[i]);
499-
result[resultIndex] = 0x18;
500-
result[resultIndex + 1] = bytes1(val);
501-
resultIndex += 2;
513+
if (val <= 0x17) {
514+
result[resultIndex] = bytes1(val);
515+
resultIndex++;
516+
} else {
517+
result[resultIndex] = 0x18;
518+
result[resultIndex + 1] = bytes1(val);
519+
resultIndex += 2;
520+
}
502521
}
503522
return result;
504523
}

test/LibBlobs.t.sol

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ pragma solidity ^0.8.26;
44
import {Test, Vm} from "forge-std/Test.sol";
55
import {console2 as console} from "forge-std/console2.sol";
66

7-
import {Account as CreditAccount, AddBlobParams, Approvals, SubnetStats} from "../src/types/BlobTypes.sol";
7+
import {Account as CreditAccount, AddBlobParams, Approvals, SubnetStats, Subscriber} from "../src/types/BlobTypes.sol";
88
import {LibBlob} from "../src/util/LibBlob.sol";
9+
import {LibWasm} from "../src/util/LibWasm.sol";
910

1011
contract LibBlobTest is Test {
1112
using LibBlob for bytes;
@@ -100,14 +101,62 @@ contract LibBlobTest is Test {
100101
);
101102
}
102103

103-
// function testDecodeSubscribers() public view {
104-
// // One subscriber
105-
// bytes memory data =
106-
// hex"a156040a90f79bf6eb2c4f870365e785982e1f101e93b906a26744656661756c74861903d1192b8cf59820181a187918eb18c1051858185b188518a518ba18a5187a18d018b1182118fc18ec189d189b185418eb0c18ef18ea18f7189e0f1866185411185818aef6f4a1634b6579982018a118a7182203183b184c184d18b0186e18c918c018a718e01899186f1821184d18a4189b187318f318df18c0187618e61518d002182918d4184118d086190f77191d87f59820181a187918eb18c1051858185b188518a518ba18a5187a18d018b1182118fc18ec189d189b185418eb0c18ef18ea18f7189e0f1866185411185818aef6f4";
107-
// // Two subscribers
108-
// data =
109-
// hex"a256040a90f79bf6eb2c4f870365e785982e1f101e93b906a26744656661756c74861903d1192b8cf59820181a187918eb18c1051858185b188518a518ba18a5187a18d018b1182118fc18ec189d189b185418eb0c18ef18ea18f7189e0f1866185411185818aef6f4a1634b6579982018a118a7182203183b184c184d18b0186e18c918c018a718e01899186f1821184d18a4189b187318f318df18c0187618e61518d002182918d4184118d086190f77192b98f59820181a187918eb18c1051858185b188518a518ba18a5187a18d018b1182118fc18ec189d189b185418eb0c18ef18ea18f7189e0f1866185411185818aef6f456040a976ea74026e726554db657fa54763abd0c3a0aa9a16744656661756c7486191d9c192bacf59820181a187918eb18c1051858185b188518a518ba18a5187a18d018b1182118fc18ec189d189b185418eb0c18ef18ea18f7189e0f1866185411185818aef6f4";
110-
// }
104+
function testEncodeDecodeSubscriptionId() public view {
105+
// A small key string
106+
string memory key = "foo";
107+
bytes memory data = LibBlob.encodeSubscriptionId(key);
108+
assertEq(data, hex"a1634b6579831866186f186f");
109+
string memory decoded = LibBlob.decodeSubscriptionId(data);
110+
assertEq(decoded, key);
111+
112+
// Max 255 bytes
113+
key =
114+
"foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo";
115+
data = LibBlob.encodeSubscriptionId(key);
116+
assertEq(
117+
data,
118+
hex"a1634b657998ff1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f1866186f186f"
119+
);
120+
decoded = LibBlob.decodeSubscriptionId(data);
121+
assertEq(decoded, key);
122+
123+
// Default case
124+
key = "";
125+
data = LibBlob.encodeSubscriptionId(key);
126+
assertEq(data, hex"6744656661756c74");
127+
decoded = LibBlob.decodeSubscriptionId(data);
128+
assertEq(decoded, "Default");
129+
}
130+
131+
function testDecodeSubscribers() public view {
132+
// One subscriber
133+
bytes memory data =
134+
hex"a156040a90f79bf6eb2c4f870365e785982e1f101e93b906a26744656661756c74861903d1192b8cf59820181a187918eb18c1051858185b188518a518ba18a5187a18d018b1182118fc18ec189d189b185418eb0c18ef18ea18f7189e0f1866185411185818aef6f4a1634b6579982018a118a7182203183b184c184d18b0186e18c918c018a718e01899186f1821184d18a4189b187318f318df18c0187618e61518d002182918d4184118d086190f77191d87f59820181a187918eb18c1051858185b188518a518ba18a5187a18d018b1182118fc18ec189d189b185418eb0c18ef18ea18f7189e0f1866185411185818aef6f4";
135+
Subscriber[] memory subscribers = LibBlob.decodeSubscribers(data);
136+
assertEq(subscribers[0].subscriber, 0x90F79bf6EB2c4f870365E785982E1f101E93b906);
137+
assertEq(subscribers[0].subscriptionGroup[0].subscriptionId, "Default");
138+
assertEq(
139+
subscribers[0].subscriptionGroup[0].subscription,
140+
hex"861903d1192b8cf59820181a187918eb18c1051858185b188518a518ba18a5187a18d018b1182118fc18ec189d189b185418eb0c18ef18ea18f7189e0f1866185411185818aef6f4"
141+
);
142+
143+
// Two subscribers
144+
data =
145+
hex"a256040a90f79bf6eb2c4f870365e785982e1f101e93b906a26744656661756c74861903d1192b8cf59820181a187918eb18c1051858185b188518a518ba18a5187a18d018b1182118fc18ec189d189b185418eb0c18ef18ea18f7189e0f1866185411185818aef6f4a1634b6579982018a118a7182203183b184c184d18b0186e18c918c018a718e01899186f1821184d18a4189b187318f318df18c0187618e61518d002182918d4184118d086190f77192b98f59820181a187918eb18c1051858185b188518a518ba18a5187a18d018b1182118fc18ec189d189b185418eb0c18ef18ea18f7189e0f1866185411185818aef6f456040a976ea74026e726554db657fa54763abd0c3a0aa9a16744656661756c7486191d9c192bacf59820181a187918eb18c1051858185b188518a518ba18a5187a18d018b1182118fc18ec189d189b185418eb0c18ef18ea18f7189e0f1866185411185818aef6f4";
146+
subscribers = LibBlob.decodeSubscribers(data);
147+
assertEq(subscribers[0].subscriber, 0x90F79bf6EB2c4f870365E785982E1f101E93b906);
148+
assertEq(subscribers[0].subscriptionGroup[0].subscriptionId, "Default");
149+
assertEq(
150+
subscribers[0].subscriptionGroup[0].subscription,
151+
hex"861903d1192b8cf59820181a187918eb18c1051858185b188518a518ba18a5187a18d018b1182118fc18ec189d189b185418eb0c18ef18ea18f7189e0f1866185411185818aef6f4"
152+
);
153+
assertEq(subscribers[1].subscriber, 0x976EA74026E726554dB657fA54763abd0C3a0aa9);
154+
assertEq(subscribers[1].subscriptionGroup[0].subscriptionId, "Default");
155+
assertEq(
156+
subscribers[1].subscriptionGroup[0].subscription,
157+
hex"86191d9c192bacf59820181a187918eb18c1051858185b188518a518ba18a5187a18d018b1182118fc18ec189d189b185418eb0c18ef18ea18f7189e0f1866185411185818aef6f4"
158+
);
159+
}
111160

112161
// function testDecodePendingBlobs() public view {
113162
// bytes memory data =

0 commit comments

Comments
 (0)