From a50a7bb435e1cd3baf75f3a738fc1b741c63cc02 Mon Sep 17 00:00:00 2001 From: Matthieu Vachon Date: Thu, 13 Aug 2026 19:37:43 -0400 Subject: [PATCH 1/2] fix(test): make complex_substreams testdata crate compile again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Its generated `pb/mod.rs` declared four modules — `google.protobuf`, `sf.substreams.rpc.v2` and `sf.substreams.sink.service.v1` — whose `.rs` files are not in the directory, so the crate failed to build before any of its own code was reached. Drop the declarations that have no backing file. This blocks rebuilding the package's committed .spkg. --- .../testdata/complex_substreams/src/pb/mod.rs | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/test/testdata/complex_substreams/src/pb/mod.rs b/test/testdata/complex_substreams/src/pb/mod.rs index 527e59ffc..deedff8a6 100644 --- a/test/testdata/complex_substreams/src/pb/mod.rs +++ b/test/testdata/complex_substreams/src/pb/mod.rs @@ -1,11 +1,4 @@ // @generated -pub mod google { - // @@protoc_insertion_point(attribute:google.protobuf) - pub mod protobuf { - include!("google.protobuf.rs"); - // @@protoc_insertion_point(google.protobuf) - } -} pub mod sf { // @@protoc_insertion_point(attribute:sf.substreams) pub mod substreams { @@ -18,22 +11,6 @@ pub mod sf { // @@protoc_insertion_point(sf.substreams.index.v1) } } - pub mod rpc { - // @@protoc_insertion_point(attribute:sf.substreams.rpc.v2) - pub mod v2 { - include!("sf.substreams.rpc.v2.rs"); - // @@protoc_insertion_point(sf.substreams.rpc.v2) - } - } - pub mod sink { - pub mod service { - // @@protoc_insertion_point(attribute:sf.substreams.sink.service.v1) - pub mod v1 { - include!("sf.substreams.sink.service.v1.rs"); - // @@protoc_insertion_point(sf.substreams.sink.service.v1) - } - } - } // @@protoc_insertion_point(attribute:sf.substreams.v1) pub mod v1 { include!("sf.substreams.v1.rs"); From 120f627249b13a9c93153d05f344f62351f75d6d Mon Sep 17 00:00:00 2001 From: Matthieu Vachon Date: Thu, 13 Aug 2026 19:37:53 -0400 Subject: [PATCH 2/2] feat(wasm): add a `context::clock` intrinsic Modules could only reach the block clock by declaring `source: sf.substreams.v1.Clock` as a module input, which forces the dependency into the module graph even when the clock is only needed occasionally. Add a `context` host module exposing it as a call instead: #[link(wasm_import_module = "context")] extern "C" { fn clock(output_ptr: *mut u8); } `context::clock` writes the clock to the guest heap as an encoded `sf.substreams.v1.Clock` and stores the `{ptr, len}` pair at `output_ptr`, the same convention the `state` getters use. It returns nothing: unlike `state::get_at` there is no "not found" case. A marshalling failure is raised as non-deterministic so it is never cached. Implemented for wasmtime (the default runtime) and wazero (which also covers wasip1/tinygo-v1); the JavaScript/v8 runtime is not covered. `context` joins `env`, `state` and `logger` as a namespace WASM extensions cannot register into, keeping it available for what lands there next. Ergonomic Rust bindings will follow separately in substreams-rs. Verified end to end by a new `assert_context_intrinsics_0` module in the complex_substreams testdata package, which asserts inside the WASM that the intrinsic matches the Clock it received as a module input, exercised over blocks 0-10 on both runtimes. --- docs/release-notes/change-log.md | 4 +++ .../complex-substreams-v0.1.0.spkg | Bin 247980 -> 251549 bytes test/testdata/complex_substreams/src/lib.rs | 33 ++++++++++++++++++ .../complex_substreams/substreams.yaml | 8 +++++ test/tier2_integration_test.go | 32 +++++++++++++++++ wasm/call.go | 12 +++++++ wasm/context_test.go | 25 +++++++++++++ wasm/registry.go | 3 ++ wasm/wasmtime/context_externs.go | 24 +++++++++++++ wasm/wasmtime/instance.go | 4 +++ wasm/wazero/context_hostmod.go | 29 +++++++++++++++ wasm/wazero/module.go | 6 +++- 12 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 wasm/context_test.go create mode 100644 wasm/wasmtime/context_externs.go create mode 100644 wasm/wazero/context_hostmod.go diff --git a/docs/release-notes/change-log.md b/docs/release-notes/change-log.md index 23fff90b4..75c103eee 100644 --- a/docs/release-notes/change-log.md +++ b/docs/release-notes/change-log.md @@ -11,6 +11,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## Unreleased +### Added + +- WASM: new `context` host module giving modules an intrinsic they can call at any point during execution: `context::clock(output_ptr)` writes the block clock as an encoded `sf.substreams.v1.Clock`. It writes a `{ptr, len}` pair at `output_ptr`, the same convention the `state` getters use, and is available on the `wasmtime` and `wazero` runtimes (not on the JavaScript/v8 one). Until now the clock was only reachable by declaring `source: sf.substreams.v1.Clock` as a module input. Ergonomic Rust bindings will follow in `substreams-rs`; until then a module declares the import itself with `#[link(wasm_import_module = "context")]`. `context` joins `env`, `state` and `logger` as a namespace WASM extensions cannot register into. + ### Fixed - Server: `substreams-tier1` now restarts when its block hub can no longer link incoming live blocks, instead of hanging every request at a frozen head indefinitely. A live-source gap whose one-block files were already merged away can never be linked, and the head-block metrics keep tracking the live source, so the process looked healthy throughout. diff --git a/test/testdata/complex_substreams/complex-substreams-v0.1.0.spkg b/test/testdata/complex_substreams/complex-substreams-v0.1.0.spkg index c974ea7e52bb54c24a86551266a0af02eb1398f2..be40baf99459467224921eab7e4bc74e1b7f27cf 100644 GIT binary patch delta 44694 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 delta 41096 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 z zo$2WnvfXJ<()@LW93DweLd%j&!^bD}__jfO8gd{9OyevLI{TscB%x-kBux!a0L#}= zYG4@wr-QtS*ob6$G5MxmedTxzcg%BU($HMK=S}biS)4VM0~MdugD2aYI5dKzfP9w& ziW@#S0Z}h+Pld9HgL>Fc<8ynHd@aEHoYeduR8BVWvA+urBor8$jt#reo*<&MI^{-1 zyk`sQH!NL$3p1r*hG}?PrVsBS`OOx%Ey0z^GK)!Sw?((Kb zTZ=V4GvR7~iv~?L9D8SE8F4xN+Agw4)NvXFKk+Yr^O5KEqEhBGR`-m6r=!yacdY}99~2P3@Qo7Uac{f z;<4me3y0nswhh>Z3Xlrav`m_W2ug!QE`N*her$ zExc}SEVP*xx;^%^sdFc@NM_>ss+z(u?v4aWw@B@Y> zJ%)+j@JG;G=O}KfeWI2V01mHYeSDEO^LGGX%r`kG$5gTldC?fF5c4PY?|}enAn&My zP}luv@g*<-8a()!nmiQ1!y222ve+6+=OzR(pB25)mamGJzd9h$Xs`nmBG;V9`wY!w za}mFtTbH7-t72n;TJ&HnzLTQ!rZW3BSohmgY3czD>^D4m9V~THHKzp4`IIDwG z0avV%o!hKf&Gp&}FiS8CDm}9tmkBptB6c0g&SD;JwUoJ7Kzsc@S=sI#E@?s~WS9P( z+$N8{Ll-Ko)|@LgKw?{W^k&oK4RUDGGM#BV?c4y}8wM$^5zD5O`=y8eK2HuyepXu* zYhItQ_9oE{vadXPFTHsK8no|TN}n$;YW)I_IgG#ru67%HP+*h3K);^Zjd!g()S_2w*)?oMDHo}c~r12zVC=sf%<$MX_b zl=e5=rcIA-!`a@F`+?1~uh5kHC2rq)o7UZLVYUB$8RnV?JRon(-?JVFSxkjxbH+1~ z@4>hfuaoQPqX*=rus!-eC9^&K@THJe~-rN`G@5fVrSE%o8=hQ`Z)J( z8t;o_&m$AiYruo&-HMIrx@sSvilLs%flo-WgfMn z=)s-xFS*+{Bi-z7F4@BG-NJn15&rnS^6aC#cF7*O%OB@(k@=~9uaoA2-*Lnii45=W zl1t=An`rX$uu6YyTJ^jfBjwSD=pTQ8H>5c{l|4Fuv2B!Q;KL2YCk$>-%{b}HG>7$l= zrT@PuHTT!%z5f;2_-peHe^r(zXyHObUYGeUme{m+zU)M|y)Kgrbqb3(kP9N<4M*Zx z!8hr5ugk&BnNBYj>AFATIOi9x-`0-WChvZ1LJ*sqE_+ATtKb3cYy8pX zYv^O(>O}KsTr6M91?9!uYsJ#{2e4xHNK=RR<^3*rUtj)67WLf!5)kyfXTx!`*gc}7 z{%u8JaDc!V3w~vuc!|7+pl!^3nMNOi8yYR0e@MO%j5>e02y{*8Oi*F?q77^odEFs^ zG`Bg#fOB#7$8vJYV=p36J9m$N44=p+)aS75o3qpLKr*=W*6ar4rQ(n2_QSHU&xbk{ zu4jW->jGnccvZVcT^sd4Mg!*INm;Gyrzm)BA zqO7Fr)T05GpXV4Y`BIk0WZs9&B~huMGiSn>Hvf#Z5tzX|u(1o2eLz@?Yi2ph`%3!8 zMC;V$GNPlB_l?>m;zf%d7L3Q&*06|HzypaZXqFr+P(k1L3OuK)AYQm=(&}ZrZ|J+P zWcRM|!Uf4rlUmp{MuiIvKPG*-@ik%}2jj(zf`z-Aj>$`6*y?Y`VpqN>{cGHS8jsC? zY;Lr2CLqWR)^H?S3=P-lu62Z#$HvL?t)^ki<`#KfwdQ)AH#%4nZNZ~^k(ZI91JSB6 z&8_7Ing|Q{xCWSxQ^BV}BFc8;{sv~%cXO%RH?m#!F)m{Xi72T)rAxk%c`4BaD~f!B zWyWaDv<3iTg1f)LZqws)Nqj4B&;RLQ_wl{OKuCB#ZrbuKCM}HjDgVISpZuNd64M>M zzmp?lpx*JFoLUek?k%QFw4g!vo=d*(WiX}55uooI({HH~h7$t|E{D+(#i1wR?#mj2&tE5;=!&`mO zYl(1hHuuixf68;j$4wjmiA~|Zj@0u`%1{g$pFgR|c;Mtu$*AD1ba?A0WbFNe>`-9O z!+Zfe)9wdmFJB@%(>Or0e~|N9XuShBO=;?WOzwZnmx1Wp|CXa-`oH)`?1uTQspdy( zhWM}bzfb4lq6cj2{EJZUC+)EHxhAQK+TLJK5+|9WY9MAezI#M@erP%&)y1%VIo-B;?<*qd!4)4MYj7`@t2%Pf6?xgdY)(+@zRg{r3{oST5Z+QB}(C=h8KaYC&}4 zu`sa)e*6!p$lUupSGSnjB&q8;|MDaiJ3qafq)M`E{JCl)?Jgk{u1cook6>f=m}J$a zAj-;|!Jc8k7eWiZPiaZAn(O;%9_XFeBg~o$_|x+#m87UgQli=DvlR6&bjv5H>OxO^ z<5e2Hnx|TK=7YvqlmY+A=5}lH@L6X-#Z+i`KB9$bYEXwmS~#&c({WQ-I-J%P;BwR` ze3_=&$LRI#+$u)bX!?y?wGf!W>(W&)@VnU2h{M&`7%t#vPIA_aY!#7D<>ImwOsg-Z ztByG7@I|`n-?v%EPL9>F8Fzh7+u?Ei7!40F1{);-UD-nACv9Xo4ZqN>EmT32y|%Vc zx1y_?`n6O8B|If_GE}!%4@nDNoHJdgxau*l2~^jz5KtWvFcSh74?Fy$dqc zj8@y3g&a487P5Qk{!G>H|7WceTVON`Gu5;!Z~Aj9HC&S8B_bQT=iV56>KX& z2x?kt4fP|Mm!~RXMBpFtRKL#g@?T3*z5oDR$IR3&^Zw-l(wNXjWxccn!2XL_$CQ%Jf(TpPL~d93GYCAri0o8(LBGS8p^@fI;xzM=vE*))=^E# zitCkhtQ#4gqco$FIwyv|{?tib#zgPhS#@ZZx=j~#R=F~NzIL=Z?h;;Mz*>;G%!;Mk zyQnXslbOW6N%M%deNXUT&*G-#T~$bwY>0!=5_E$z(LDSD=dmGU0J4zP4ioGs(~A_8 z*TWC z-OGT>b)DcN2HK$V#U}iDkxtOYQ(VH{jXGiLi_{{Z+T?2b$7irWKJp?B45;jM&EUv@ z`&Kr;*i;ix-wOH0i`MVLj4x67ZT4GDVV<*&%(17`Y3#(FDw&=sQN4!lj?IQOJrAy% z@R$qZV%_+m6@h0}Y%J6dkQQ2@cELJT4+*N4Nqa0(HC+}|ZAI?u7Jur@;8E!2EH|su`%ceokL^$Mxb8>WQCwhm;fdFp#+E7k^w1vm{XywJOoint zzxp8WNY$H%d7TC)ym&M6{6BLZy-;I)3Ukq0i1Iwecp7N|1D(ht8-kR1#EjbMOiFlD zgxPGWuP#@&VPjeM3e}UhvRzuChPf>y*d_-ak5{O!n5EvYPz8`|Co7aM<#nzN=hBW- zr%Kho?S}naxw8R9@(OM#&;zvVD^-Umv;affQmF!R(QEW!r7CQBlJ;kH4q_xANBSxxObCz*9%N{z3Zj0uR0f7MyB*t zBiswQ`54l?nT=lRt9tO(=O6mvFu^;N(NDF`*~9JPy#dKySFW`q3c3U`59p^l1Im^C z)H(4`p6{o6|7s{_^jG~rYCgt*pT<1eAFO4cV(2kIIrng|3J46)ZJ1Q8Qw%?|@js8& zl*uI8JV3YbhXD{B_OXWBc*hoa0DOJwu?Bi#pql8}!Q0^*_?HR!ln8!FFkq@)?)`_j z`7Rs{LCknlJ)Pe8F=DFH(hRSG+b2o}scXd=+B`@Vv?Z|!;n1323z8;^UxvPV7(%>8OWr8-5Upd5YC|?ER=fnqGg^GQ}%;7~b9!jlM_(5+41NuU^qb zCbOQ4xGoaEt&xW>Y38C$IKF}}zFU&F>;WHKTHauxlrGrY7%;!GF+!lUnLNCo2Nh(Q zTl9pK?E$Mxb64}m3#uNfI%Tc{U6{*2bU2XVJSMt(s4DueemS`5i=oPs;CYalE3Jj@ zY>fN|D1D^5-oGh6cB2laJvmkwrv7#9&0}fvNENuyXBb8fo@6|JJniwg@wo7Gz|#>= zmL2Yd_nFfsPyEe{`iiP)(C!Qkik3m z45JC}ZaUs$q~gu>iI{LcjT)tbbnslLRZf{K5~EakrW3qMcz9A`(8ETlOQP}H5kK6D ze{z&6@Lz+fxC>8COMdG)&)*Qav-Be7K?4&PSp1V&eNX$6tM(>fOW1KR6n1)pIt& z1$dlv4kwUrw92M$&QtBqbK=iJd`e8dbMYNZg3$=a!r^d2O#a{C+bU0&pRX>Al4&nJ zd%kLu+QH>9TIlTb!TG9d^*a}J!?irI_hA;~ar%O1@6?Deq#>4Wo2u~M0 zUFnsJR2%$=<)t9Oe?HsMzCG1M^7?;vVqg5XKfPBJz?ro65|x;+2cLSxkawlAg@KA)g_wZAJ z{FC%XUp%KGGaSbPl_z3sK-HHceW|oyJK-jTxt{Y`1fCI|Fr#{W_}a-6>c(DOJz-{e zQurF{WAc>nwCWk-CQq6?t!~_;>Iu!+r`K^I{yT2sxEbL(?}YHAD`#Bg4bQ3#hwH+1 zGbe>-O`+wNtFA2uPncXge#W?o;psD~Cr+W0pmmSmyFI$SeS2B>u@qwliS1ctp#FGz zQT1i8v;(y1GBq;a;RS_>jitl@JOlACB}_`VT$PabQsv7_0|kAEJD%my>~<=JMn)uXvQ-<|Kl?J4B`O( zIboh6$4vrN$JKeS88mnyjtN6{n*&j-lL^a&CL4x z@HFqtNp<0A^%EvvLsyJd7|6@7P&qkgA@4bOF2OSn&vZQ3;h`@sSDmG?m$JvIPP7#j zq!`~LzAfO=uFF+k25T>aCQqIauAVe4JbmVb85Foel_&7XID;nQGiAQV(o;6jmMc`B z%$E`LAoX3m+u;4RhyHnm@|QXBSs7M(2fUexUn3vC`JXdJtMGj$o*^`H2E* zZN^oZ9wQgA5oBj&>MELBqXwsJKs1k*C+U?M)h6W#f?WTT^hJ&8mXh4cW3)!FfPA$o znA{gZrXf|=s_SSn4YauecFBs zFwA5Q6P-Yqkx8bEuo~j)&I&Dp<#=-oF2g$+?=ao~glyM_RbhscZ5g5&_hi}>R_*2P zYy?8ksXyd4`jPr+8-8G`e}Fiuf+==+TqTU3jDSjl1#oSme zx5m@e4!iO8;>pC*2~P+Qm*pn~-}!iEVFZ=i@h$OgkEaX||6`)?^BX)D;(_dIp(8Fr z;9@+ZDSWLOBf8V`*FuhWC-GaTA#Ze{&~Mdw86CP>L%jnsf)vKtboXynH(xTMd4Svk zaxza}f`_Z)ipJo%ls@^b>Xq;$vU$n;t!hQxuTy#QU{@M;9o&t1H1j$&GU0-59;4Js zK5(6Kn@bR8s0;A0s7}S>#NCS8xufhR=$bA=nE$b8z3LpRxw+@43lfdRJv_!06rQ67Cs?0b=BW1RvwM29gtwyJ zZ%*wPQNiv9kjj$!INmH`buPS@A(BaRs@4!9)YUrIG437pTg-TUsUhET60L3oEPZ>c)#9^47j74Xs)Aw_B_)+5)n(=NfwHoS zP<61%HIS2O$y}A$sw`YzR$E?EUmgsW2Z{kRA&A5_1?5wwq1p+PP`p7(`SsW3s+Bou9rgHZ%}8^nt7^qW}vpZrY=xXSsbhgR@4-iSEW+F#p;60 z>Tq#sWp!&r`^^#$tc zimPj@+EUw_RENw!C3?QTK2%x(ila}f+R@0H)PO}q zkIvYnI%U)a>q`N-w6-=-TwGjH<)^+Ifkrr3QC4145h$w+hAP6LU=@y>ay?BOQBMbY zV-eOCI@11)s(og0Nua)@Bp50URseimaaAYX{>*Twq`tJSK3rW|UQ<$BUS8FiT5Ljo zuwQw&t|kC(EGe!I7FQL~>`m(IjN)KjMSXpJ8L%j>uB@->Lf(f!sM-+scd)Fut~6X6 zDl4w)O26UwnowCN5GpUJ42EkfDr@Viy6L{j43&h-@UNye5G<=Js|Z$gr;#_ScA^J8 zHeY0kp8RShgS}|f0+9_n&olGRmHRD;hUA8&b~#RnE@^+ z4%U|is>_14CB?z261wjeuw#(c-3*AObZ{XcmC@20MLQV?QNgXMhbX7BZ&e*L!`0Qb zLEw)HK%JU!RRw3u6_wO@n`)y<1HCP*OX{j?Le=4#@?dpYxH?c*)rXGV3a4Cob!|y) zxT3PAx+Gj#5)4%Jwc<;7#FSLmmWJvoYl};YtNL;O)QbM(UaSf;>#GB`<)L68R1&NV zRD<^hSR=8vG7u~-D=q_rRTS5RYO4lX@xi)4puDc8vaU85457=bOpAFcDgz~`s=B5! zSX&w>4Ob1K7JF1>W?iVZyso}BR9RAqJ}a)T8qDo!E6$*MZc~?vD!TO^=(@$hnqYOH zCR`B!5m0T_nKa;btW6D}eG5dMIE&WbB-)FiH1=jtWEky4TK4cvh?TL`(_wDUutsA^ z;A|>hBwC3P7EZ;GfZ=is@{;=UN-%BJIdpUp7gVl_!3prsti)5B2-daQymPJ)&|2>=eMTnLT*oq3SX*>?%uZ%eX5_QK+=)Vp?#A3TFoEt4nGFAxJ6A1EqCkRio*LJ1}}L z(Tvlo4Du7Qp)6EeQWq!*1gpjjqP1&)MC49z*QF$usX>{SIl_3FC5$g0#$u=o%~_^= zVl1s&rq0F$`o%Kr3af4wK0#4KbM6LnZ@5ckh*}=4rS!MEu=HI=|GEn%W%xWQT&2zv z^>od0H9}r_8JVwR`TnZ0{2o9>kKmNfIGX&D^3wgJGGs$NjbEWg(77vAa%N>&Fj!Gn zQde3NtPBN$rB&mpVTBsgs-hAiw6++nt*$I9t*EV<(2SA46*@9p2YFFbQW6N%gQ^pU zS;BJk-I$$%C7@|dMF;~l5CBtEP0|g>43rj^l~>f%1j;ZLO6x*ZlP%FxR$W|Ejp?km z9^(fTu9~8;#Vl8Y%1c6^dk}**&ZT+@Lm_2Cr7V#q~62wVEvE(3h*>$BIzbH7ckYAqm#+RD~4Tsgmf% zH7ZBW6|`oJs=@`ubQoFW4T5^V1VIWC7Gqohk1|l9JXAHG&R(l7$}9;5gW-}usG=rN zQ&C)7UUeh=ZmsI07KoUR!wg*t<_*@@l-E^<%B!oZFr>#`a!#3EA1TwmGw2?bp<0Z- zKy77dMNJrzt7;*gy$*v3^Kc!sf|}w0Bo>Be)y;I>I@K*R1c6jjTUK0CQ&$(NudJ)O zg|@F#oioeB;hKtCCM>!;SQ@ChmA+o5x@SUpz#LRrUx^tPuac@o)N8#e%EV+}84MKH z)|cXMO|YbDG2MEfDkvB?eeBFhwv_NTlp+_Z5YYlueH94;<5i zMDcCIv?5iNbNEm>o@E7&9y*%k`+yY9fIxVe&#eG`dX8nG4b)J8VcU_M%VR4D`M)-g zn<_BrwhpuaEM&AGGBoI_`TTVS$kA21w(?c zPj3ISr0Yfo&oa8eymhK_nN(!x)i<#zaT~kurK{K!{0>r+8?785HT%vZ85G z5RWhZ&Ib6yXan*?zRCkC>Eg5pw}@2?WUV7Im>c|HMQW9c1HKTOkjk88Ngi(xi~hzAPvvcKrZOVgCy`(xrB39 z#ES>f<@&ng1ZohPkc~@u%!=43M7?8x-7+Co#SwBDKOfgOGA>|5jS%6w4Wg58aQg~4 zjcEji3hAsO!XU&VS0CZ*4Gqe3+>GUU zS)W!RTU1#E6h7KamvCVN*0*yt@(4|HSMk_tx{S}O1{1s)UwfymR!+()?P}%D7?$rK zr$&i^`KsJdHBHTqLdP{MII$_$C|uY}MFTa%tp%_jN9$oJwT|m?%d{6jWte(k8a{Z? zbL3hcwwCTr+b)7i2uKR}kPrT^=lsiynm;*p`oRfiGhr zCEv=nzEA+rW08oY#Mc7Xm2WFK+2qBK9w;sa1Uv@Szggk6J!J9i^^`*`ij!dbQ}qxk z@9-P-bQ6B?+gEQMNn}Xc2kPL6HH`V`+ihuT0~z4lMJfo5@o? z=GmJeRQr?39MOWl5Aci@I){6<<8<1D7CaX^7#ED9Lr~Emu_3&_1$yQaKGs4-boc~j zi{J88u!W2~EwBVo1?ekvoNyxfDV!U@1dv!V121TR|TbAKDsyXh^6`Qu|rvuH>C z?-HYjFw8lHi`((TeBUcK#EphI^H+rrZ>1MfzbRbWK@TgxC)0h-HZ=c3 z;Y+sBxYVBtKedfUD@XrbH>Bps;gGYrt^>A^e?ewuhXoCB8jsvgIvrDx=*AKnGF?71@g+ z&?jOu5T-7l<6&J?iW6h=y5M{-@ULAo9xnyEc_%GaUgXbq(u7lIrJjVgu+1W#v=c(K za0dgw%ca&1<#&6E`n4E)mN4 z-8iE*pKt6&>@DCq-82^Og*ecSU|z^q?xE-KAMovafHSWy?4f5aU6i^A-D~QUNBt>N zA-PMYm=BhfJp}#?4cmr_&{bVnCUSc4Va|Ja%J^vymhScB`m$bJFXoZGF?9fe*pCp-53A> diff --git a/test/testdata/complex_substreams/src/lib.rs b/test/testdata/complex_substreams/src/lib.rs index 71b7c3bc9..4204343b1 100644 --- a/test/testdata/complex_substreams/src/lib.rs +++ b/test/testdata/complex_substreams/src/lib.rs @@ -326,3 +326,36 @@ fn assert_set_sum_store_deltas_0(block: test::Block, set_sum_store: store::Delta + +/// Raw binding to the `context` host module. It lives here rather than coming from +/// `substreams` because the crate does not expose it yet. +mod context_intrinsics { + use prost::Message; + + #[link(wasm_import_module = "context")] + extern "C" { + fn clock(output_ptr: *mut u8); + } + + /// Reads the `{ptr, len}` pair the host writes at `output_ptr` and takes ownership of + /// the payload it points at, which the host allocated through our own `alloc`. + unsafe fn read_output(intrinsic: unsafe extern "C" fn(*mut u8)) -> Vec { + let mut output: [u32; 2] = [0, 0]; + intrinsic(output.as_mut_ptr() as *mut u8); + + let (ptr, len) = (output[0] as *mut u8, output[1] as usize); + Vec::from_raw_parts(ptr, len, len) + } + + pub fn get_clock() -> crate::pb::sf::substreams::v1::Clock { + let data = unsafe { read_output(clock) }; + crate::pb::sf::substreams::v1::Clock::decode(data.as_slice()).expect("clock is not a valid Clock message") + } +} + +#[substreams::handlers::map] +fn assert_context_intrinsics_0(clock: pb::sf::substreams::v1::Clock) -> Result { + assert_eq!(context_intrinsics::get_clock(), clock); + + Ok(test::Boolean { result: true }) +} diff --git a/test/testdata/complex_substreams/substreams.yaml b/test/testdata/complex_substreams/substreams.yaml index b0d86a332..52acd3091 100644 --- a/test/testdata/complex_substreams/substreams.yaml +++ b/test/testdata/complex_substreams/substreams.yaml @@ -35,6 +35,14 @@ modules: output: type: proto:sf.substreams.v1.test.Boolean + - name: assert_context_intrinsics_0 + kind: map + initialBlock: 0 + inputs: + - source: sf.substreams.v1.Clock + output: + type: proto:sf.substreams.v1.test.Boolean + - name: map_hybrid_input_clock_70 kind: map initialBlock: 70 diff --git a/test/tier2_integration_test.go b/test/tier2_integration_test.go index 8fa9de478..70627641c 100644 --- a/test/tier2_integration_test.go +++ b/test/tier2_integration_test.go @@ -56,6 +56,7 @@ func TestTier2Call(t *testing.T) { mapHybridInputClock70 := hex.EncodeToString([]byte("map_hybrid_input_clock_70")) mapHybridInputBlock70 := hex.EncodeToString([]byte("map_hybrid_input_block_70")) setSumStoreInit0 := hex.EncodeToString([]byte("set_sum_store_init_0")) + assertContextIntrinsics0 := hex.EncodeToString([]byte("assert_context_intrinsics_0")) randomIndicesRange := roaring64.New() randomIndicesRange.AddInt(70) @@ -357,6 +358,37 @@ func TestTier2Call(t *testing.T) { mapOutputFileToCheck: mapUsingIndexInit70 + "/outputs/0000000070-0000000080.output", expectedSkippedBlocks: map[uint64]struct{}{75: {}, 77: {}, 78: {}, 79: {}, 80: {}}, // faked with the randomIndicesRange above }, + // The module asserts, inside the WASM, that `context::clock` matches the Clock it + // received as a module input. A mismatch panics in the module, so a `true` output + // for every block is the proof that the intrinsic is wired correctly. + { + name: "context intrinsics expose the clock", + startBlock: 0, + stage: 0, + moduleName: "assert_context_intrinsics_0", + stateBundleSize: 10, + manifestPath: "./testdata/complex_substreams/complex-substreams-v0.1.0.spkg", + + expectRemainingFiles: []string{ + assertContextIntrinsics0 + "/outputs/0000000000-0000000010.output", + }, + + mapOutputFilesToDeepInspectForKeys: map[string]map[uint64]any{ + assertContextIntrinsics0 + "/outputs/0000000000-0000000010.output": { + 0: &pbsubstreamstest.Boolean{Result: true}, + 1: &pbsubstreamstest.Boolean{Result: true}, + 2: &pbsubstreamstest.Boolean{Result: true}, + 3: &pbsubstreamstest.Boolean{Result: true}, + 4: &pbsubstreamstest.Boolean{Result: true}, + 5: &pbsubstreamstest.Boolean{Result: true}, + 6: &pbsubstreamstest.Boolean{Result: true}, + 7: &pbsubstreamstest.Boolean{Result: true}, + 8: &pbsubstreamstest.Boolean{Result: true}, + 9: &pbsubstreamstest.Boolean{Result: true}, + }, + }, + }, + // This test checks that a module receiving data from both a filtered map and a Clock // does not trigger on every block, even when the index is being created in the same run { diff --git a/wasm/call.go b/wasm/call.go index 0e7a466de..552727e19 100644 --- a/wasm/call.go +++ b/wasm/call.go @@ -23,6 +23,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" ) var ErrWasmDeterministicExec = errors.New("wasm execution failed deterministically") @@ -166,6 +167,17 @@ func (c *Call) ReachedLogsMaxByteCount() bool { return c.LogsByteCount >= maxTotalLogsByteCount } +// DoClock backs the `context::clock` intrinsic, returning the block clock encoded as a +// `sf.substreams.v1.Clock` protobuf message. +func (c *Call) DoClock() []byte { + out, err := proto.Marshal(c.Clock) + if err != nil { + c.PanicNonDeterministicError(fmt.Errorf("marshalling clock: %w", err)) + } + + return out +} + func (c *Call) DoSet(ord uint64, key string, value []byte) { now := time.Now() c.validateSimple("set", pbsubstreams.Module_KindStore_UPDATE_POLICY_SET, key) diff --git a/wasm/context_test.go b/wasm/context_test.go new file mode 100644 index 000000000..3405d2913 --- /dev/null +++ b/wasm/context_test.go @@ -0,0 +1,25 @@ +package wasm + +import ( + "testing" + "time" + + pbsubstreams "github.com/streamingfast/substreams/pb/sf/substreams/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestCall_DoClock(t *testing.T) { + clock := &pbsubstreams.Clock{ + Id: "block-10", + Number: 10, + Timestamp: timestamppb.New(time.Unix(1000, 0).UTC()), + } + call := &Call{ModuleName: "mod", Clock: clock} + + out := &pbsubstreams.Clock{} + require.NoError(t, proto.Unmarshal(call.DoClock(), out)) + assert.True(t, proto.Equal(clock, out), "expected %s, got %s", clock, out) +} diff --git a/wasm/registry.go b/wasm/registry.go index 71d41fc8b..906941a0e 100644 --- a/wasm/registry.go +++ b/wasm/registry.go @@ -34,6 +34,9 @@ func (r *Registry) registerWASMExtension(namespace string, importName string, ex if namespace == "logger" { panic("cannot extend 'logger' wasm namespace") } + if namespace == "context" { + panic("cannot extend 'context' wasm namespace") + } if r.Extensions == nil { r.Extensions = map[string]map[string]WASMExtension{} diff --git a/wasm/wasmtime/context_externs.go b/wasm/wasmtime/context_externs.go new file mode 100644 index 000000000..d9e93801b --- /dev/null +++ b/wasm/wasmtime/context_externs.go @@ -0,0 +1,24 @@ +package wasmtime + +import ( + "fmt" + + wasmtime "github.com/bytecodealliance/wasmtime-go/v41" +) + +// registerContextImports exposes the execution context a module runs in. Unlike the +// `state` getters the value always exists, so the import returns nothing and simply +// writes the payload (a `{ptr, len}` pair) at `outputPtr`. +func (i *instance) registerContextImports(linker *wasmtime.Linker) error { + if err := linker.FuncWrap("context", "clock", + func(outputPtr int32) { + if err := writeOutputToHeap(i, outputPtr, i.CurrentCall.DoClock()); err != nil { + i.CurrentCall.PanicDeterministicError("writing clock to heap: %w", err) + } + }, + ); err != nil { + return fmt.Errorf("registering clock import: %w", err) + } + + return nil +} diff --git a/wasm/wasmtime/instance.go b/wasm/wasmtime/instance.go index 47e0a2c70..abad3b8f2 100644 --- a/wasm/wasmtime/instance.go +++ b/wasm/wasmtime/instance.go @@ -89,6 +89,10 @@ func (i *instance) newImports() error { if err != nil { return fmt.Errorf("registering state imports: %w", err) } + err = i.registerContextImports(linker) + if err != nil { + return fmt.Errorf("registering context imports: %w", err) + } if err = linker.FuncWrap("env", "register_panic", func(msgPtr, msgLength int32, filenamePtr, filenameLength int32, lineNumber, columnNumber int32, caller *wasmtime.Caller) { diff --git a/wasm/wazero/context_hostmod.go b/wasm/wazero/context_hostmod.go new file mode 100644 index 000000000..d5ec8168e --- /dev/null +++ b/wasm/wazero/context_hostmod.go @@ -0,0 +1,29 @@ +package wazero + +import ( + "context" + + "github.com/tetratelabs/wazero/api" + + "github.com/streamingfast/substreams/wasm" +) + +// ContextFuncs exposes the execution context a module runs in. Unlike the `state` getters +// the value always exists, so the import returns nothing and simply writes the payload +// (a `{ptr, len}` pair) at `output_ptr`. +var ContextFuncs = []funcs{ + { + "clock", + []parm{i32}, + []parm{}, + api.GoModuleFunc(func(ctx context.Context, mod api.Module, stack []uint64) { + outputPtr := uint32(stack[0]) + call := wasm.FromContext(ctx) + inst := instanceFromContext(ctx) + + if err := writeOutputToHeap(ctx, inst, outputPtr, call.DoClock()); err != nil { + call.PanicDeterministicError("writing clock to heap: %w", err) + } + }), + }, +} diff --git a/wasm/wazero/module.go b/wasm/wazero/module.go index 07bcc33c9..5279fb443 100644 --- a/wasm/wazero/module.go +++ b/wasm/wazero/module.go @@ -82,7 +82,11 @@ func newModule(ctx context.Context, wasmCode []byte, wasmCodeType string, regist if err != nil { return nil, err } - hostModules = append(hostModules, envModule, stateModule, loggerModule) + contextModule, err := AddHostFunctions(ctx, runtime, "context", ContextFuncs) + if err != nil { + return nil, err + } + hostModules = append(hostModules, envModule, stateModule, loggerModule, contextModule) // TODO: where to `Close()` the `runtime` here? // One runtime per request?