Skip to content

Commit c5678db

Browse files
committed
Verify events and support recovation with merkle tree proofs to verify events
1 parent 846a1ea commit c5678db

65 files changed

Lines changed: 4644 additions & 825 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitlab/ci/build_rs_core.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,30 @@ rs-core-clippy:
4848
- cd packages/rs-core
4949
- just clippy
5050

51+
rs-core-tests:
52+
extends: .rust-check
53+
script:
54+
- cd packages/rs-core
55+
- just test
56+
57+
rs-common-format-check:
58+
extends: .rust-check
59+
script:
60+
- cd packages/rs-core
61+
- cargo fmt --check
62+
63+
rs-common-clippy:
64+
extends: .rust-check
65+
script:
66+
- cd packages/rs-common
67+
- cargo clippy
68+
69+
rs-common-tests:
70+
extends: .rust-check
71+
script:
72+
- cd packages/rs-common
73+
- cargo test
74+
5175
# rs-core-build:
5276
# extends: .rs-core-workflow
5377
# stage: build

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/polycentric/src/common/components/List/List.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,14 @@ function WebFeedViewer<T>({
159159
data,
160160
renderItem,
161161
keyExtractor,
162+
HeaderComponent,
162163
ListHeaderComponent,
163164
ListFooterComponent,
164165
ListEmptyComponent,
165166
onEndReached,
166167
contentContainerStyle,
167168
stickyHeaderIndices,
168-
}: FlashListProps<T>) {
169+
}: ListProps<T>) {
169170
const sentinelRef = useRef<View>(null);
170171
const items = (data as readonly T[] | null | undefined) ?? [];
171172
const [visibleCount, setVisibleCount] = useState(WEB_INITIAL_VISIBLE);
@@ -203,6 +204,7 @@ function WebFeedViewer<T>({
203204

204205
return (
205206
<View style={contentContainerStyle}>
207+
{renderNode(HeaderComponent)}
206208
{renderNode(ListHeaderComponent)}
207209
{isEmpty
208210
? renderNode(ListEmptyComponent)

apps/polycentric/src/features/feed/FeedList.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { isWeb } from '@/src/common/util/platform';
1010
import { Text } from '@/src/common/components/primitives';
1111
import { Atoms, useTheme } from '@/src/common/theme';
1212
import { Post } from '../post/Post';
13+
import { PostSkeletonList } from '../post/PostSkeleton';
1314

1415
export type FeedListProps = Omit<ListProps<PostData>, 'data' | 'renderItem'> & {
1516
feed: FeedHookResult;
@@ -38,7 +39,9 @@ export default function FeedList({
3839
keyExtractor={keyExtractor}
3940
data={feed.items}
4041
ListEmptyComponent={
41-
!feed.isLoading ? (
42+
feed.isLoading ? (
43+
<PostSkeletonList />
44+
) : (
4245
<View
4346
style={[
4447
Atoms.flex_1,
@@ -49,7 +52,7 @@ export default function FeedList({
4952
>
5053
<Text color="neutral_500">{emptyMessage}</Text>
5154
</View>
52-
) : null
55+
)
5356
}
5457
ListFooterComponent={
5558
feed.hasMore && feed.items.length > 0 ? (

apps/polycentric/src/features/feed/hooks/useExploreFeed.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export function useExploreFeed(options?: {
2222
new Query.GetExploreFeed({
2323
identity: identity === '' ? undefined : identity,
2424
}),
25+
undefined,
26+
enabled,
2527
);
2628

2729
const items = useMemo(() => {

apps/polycentric/src/features/feed/hooks/useFollowingFeed.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export function useFollowingFeed(options?: {
2020
const query = useQuery(
2121
feedQueryKeys.following(),
2222
new Query.GetFollowingFeed({ followerIdentity }),
23+
undefined,
24+
enabled,
2325
);
2426

2527
const items = useMemo(() => {

apps/polycentric/src/features/onboarding/OnboardingWelcomeScreen.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@ export default function OnboardingWelcomeScreen() {
2020
Atoms.inset_0,
2121
Atoms.justify_center,
2222
Atoms.items_center,
23-
Atoms.p_2xl,
23+
Atoms.gap_2xl,
24+
Atoms.p_3xl,
2425
]}
2526
>
2627
<Image
2728
source={LOGO_WITH_TEXT}
2829
contentFit="contain"
29-
style={{ width: '100%', height: '100%' }}
30+
style={{
31+
width: '100%',
32+
aspectRatio: 1,
33+
}}
3034
/>
3135
</View>
3236
<View style={[Atoms.gap_md, Atoms.w_full, Atoms.mt_auto]}>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { Atoms, useTheme, withHexOpacity } from '@/src/common/theme';
2+
import { useEffect } from 'react';
3+
import { View } from 'react-native';
4+
import Animated, {
5+
Easing,
6+
useAnimatedStyle,
7+
useSharedValue,
8+
withRepeat,
9+
withTiming,
10+
} from 'react-native-reanimated';
11+
12+
const AVATAR_SIZE = 40;
13+
const PULSE_MS = 900;
14+
15+
function useShimmerOpacity() {
16+
const opacity = useSharedValue(0.5);
17+
useEffect(() => {
18+
opacity.value = withRepeat(
19+
withTiming(1, { duration: PULSE_MS, easing: Easing.inOut(Easing.ease) }),
20+
-1,
21+
true,
22+
);
23+
}, [opacity]);
24+
return useAnimatedStyle(() => ({ opacity: opacity.value }));
25+
}
26+
27+
function Bar({
28+
width,
29+
height = 12,
30+
}: {
31+
width: number | `${number}%`;
32+
height?: number;
33+
}) {
34+
const { theme } = useTheme();
35+
return (
36+
<View
37+
style={{
38+
width,
39+
height,
40+
borderRadius: height / 2,
41+
backgroundColor: withHexOpacity(theme.palette.neutral_500, '20'),
42+
}}
43+
/>
44+
);
45+
}
46+
47+
export function PostSkeleton() {
48+
const { theme } = useTheme();
49+
const animatedStyle = useShimmerOpacity();
50+
51+
return (
52+
<Animated.View
53+
style={[
54+
Atoms.w_full,
55+
Atoms.px_md,
56+
Atoms.pt_md,
57+
Atoms.pb_md,
58+
Atoms.flex_row,
59+
Atoms.gap_md,
60+
animatedStyle,
61+
{
62+
borderBottomWidth: 1,
63+
borderBottomColor: withHexOpacity(theme.palette.neutral_500, '20'),
64+
},
65+
]}
66+
>
67+
<View
68+
style={{
69+
width: AVATAR_SIZE,
70+
height: AVATAR_SIZE,
71+
borderRadius: AVATAR_SIZE / 2,
72+
backgroundColor: withHexOpacity(theme.palette.neutral_500, '20'),
73+
}}
74+
/>
75+
<View style={[Atoms.flex_1, Atoms.gap_sm]}>
76+
<View style={[Atoms.flex_row, Atoms.gap_sm, Atoms.align_center]}>
77+
<Bar width={120} />
78+
<Bar width={60} />
79+
<Bar width={40} />
80+
</View>
81+
<Bar width="100%" />
82+
<Bar width="80%" />
83+
<View style={[Atoms.flex_row, Atoms.gap_lg, Atoms.mt_sm]}>
84+
<Bar width={28} height={14} />
85+
<Bar width={28} height={14} />
86+
<Bar width={28} height={14} />
87+
</View>
88+
</View>
89+
</Animated.View>
90+
);
91+
}
92+
93+
export function PostSkeletonList({ count = 6 }: { count?: number }) {
94+
return (
95+
<View>
96+
{Array.from({ length: count }).map((_, i) => (
97+
<PostSkeleton key={i} />
98+
))}
99+
</View>
100+
);
101+
}

apps/polycentric/src/features/profile/ProfileHeader.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ function ProfileHeaderInner({ bannerColors, onBack }: ProfileHeaderProps) {
3434

3535
const fallbackUsername = useUsername(identityKey);
3636
const profile = useProfile(identityKey, { fetchMode: FetchMode.Default });
37+
3738
const username = profile.name ?? fallbackUsername;
3839

3940
const short = identityKey ? shortenIdentityId(identityKey) : '...';
@@ -42,6 +43,8 @@ function ProfileHeaderInner({ bannerColors, onBack }: ProfileHeaderProps) {
4243
if (identityKey) router.push(Routes.tabs.editProfile(identityKey));
4344
}, [identityKey]);
4445

46+
if (profile.isLoading && !profile.name) return undefined;
47+
4548
return (
4649
<>
4750
<View style={{ position: 'relative' }}>

packages/js-core/src/polycentric-client.ts

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,29 @@ export class PolycentricClient {
276276

277277
const sequence = this.core.nextSequence(this.activeIdentityKey, collection);
278278

279+
// identity_sequence must reference an identity event signed by the
280+
// current keypair.
279281
const identitySequence =
280-
this.core.nextSequence(this.activeIdentityKey, COLLECTION.IDENTITY) - 1n;
282+
collection === COLLECTION.IDENTITY
283+
? sequence
284+
: this.core.getIdentitySequence(
285+
this.activeIdentityKey,
286+
Proto.PublicKey.toBinary(this.currentKeyPair.publicKey)
287+
.buffer as ArrayBuffer,
288+
);
289+
290+
if (!identitySequence) {
291+
throw new Error(
292+
'Cannot build event: current keypair has no identity event for the active identity (broken pairing?)',
293+
);
294+
}
295+
296+
const previousSignature = new Uint8Array(
297+
this.core.previousSignature(this.activeIdentityKey, collection),
298+
);
299+
const previousRoot = new Uint8Array(
300+
this.core.previousRoot(this.activeIdentityKey, collection),
301+
);
281302

282303
const event = Proto.Event.create({
283304
key: Proto.EventKey.create({
@@ -287,7 +308,8 @@ export class PolycentricClient {
287308
sequence,
288309
}),
289310
identitySequence,
290-
previousSignature: new Uint8Array(0),
311+
previousSignature,
312+
previousRoot,
291313
contentDigest: this.contentManager.buildDigest(content),
292314
createdAt: BigInt(Date.now()),
293315
});
@@ -575,18 +597,17 @@ export class PolycentricClient {
575597
*/
576598
async push(): Promise<void> {
577599
if (!this.currentKeyPair) throw new Error('No active key pair');
600+
if (!this.activeIdentityKey) throw new Error('No active identity');
578601

579602
const localEvents = await this.storage.events.getAll();
580-
const publicKey = this.currentKeyPair.publicKey.key;
581603

582-
// Build event bundles with content for events matching the active key
604+
// Build event bundles with content for events matching the active identity
583605
const bundles: Proto.EventBundle[] = [];
584606
for (const signedEvent of localEvents) {
585607
const event = Proto.Event.fromBinary(signedEvent.eventBytes);
586608

587-
// Only push events signed by the active key
588-
const signedBy = event.key?.signedBy;
589-
if (!signedBy || !this.bytesEqual(signedBy.key, publicKey)) continue;
609+
// Only push events belonging to the active identity
610+
if (event.key?.identity !== this.activeIdentityKey) continue;
590611

591612
// Look up content by digest
592613
let serializedContent: Proto.SerializedContent | undefined;
@@ -746,14 +767,6 @@ export class PolycentricClient {
746767
this.events.emitError(error);
747768
}
748769

749-
private bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
750-
if (a.length !== b.length) return false;
751-
for (let i = 0; i < a.length; i++) {
752-
if (a[i] !== b[i]) return false;
753-
}
754-
return true;
755-
}
756-
757770
get currentSystem(): Proto.PublicKey {
758771
return this.currentKeyPair!.publicKey;
759772
}

0 commit comments

Comments
 (0)