Skip to content

Do not allow mutable entities in entity handlers #5909

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 31, 2025
Merged
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
11 changes: 10 additions & 1 deletion graph/src/data_source/subgraph.rs
Original file line number Diff line number Diff line change
@@ -239,7 +239,16 @@ impl UnresolvedDataSource {
None => {
return Err(anyhow!("Entity {} not found in source manifest", entity));
}
Some(TypeKind::Object) => {}
Some(TypeKind::Object) => {
// Check if the entity is immutable
let entity_type = source_manifest.schema.entity_type(entity)?;
if !entity_type.is_immutable() {
return Err(anyhow!(
"Entity {} is not immutable and cannot be used as a mapping entity",
entity
));
}
}
}
}
Ok(())
12 changes: 1 addition & 11 deletions runtime/wasm/src/module/mod.rs
Original file line number Diff line number Diff line change
@@ -70,23 +70,13 @@ impl ToAscPtr for offchain::TriggerData {
}
}

impl ToAscPtr for subgraph::TriggerData {
fn to_asc_ptr<H: AscHeap>(
self,
heap: &mut H,
gas: &GasCounter,
) -> Result<AscPtr<()>, HostExportError> {
asc_new(heap, &self.entity, gas).map(|ptr| ptr.erase())
}
}

impl ToAscPtr for subgraph::MappingEntityTrigger {
fn to_asc_ptr<H: AscHeap>(
self,
heap: &mut H,
gas: &GasCounter,
) -> Result<AscPtr<()>, HostExportError> {
asc_new(heap, &self.data.entity, gas).map(|ptr| ptr.erase())
asc_new(heap, &self.data.entity.entity.sorted_ref(), gas).map(|ptr| ptr.erase())
}
}

37 changes: 1 addition & 36 deletions runtime/wasm/src/to_from/external.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
use ethabi;

use graph::blockchain::block_stream::{EntityOperationKind, EntitySourceOperation};
use graph::data::store::scalar::Timestamp;
use graph::data::value::Word;
use graph::prelude::{BigDecimal, BigInt};
use graph::runtime::gas::GasCounter;
use graph::runtime::{
asc_get, asc_new, AscIndexId, AscPtr, AscType, AscValue, HostExportError, IndexForAscTypeId,
ToAscObj,
asc_get, asc_new, AscIndexId, AscPtr, AscType, AscValue, HostExportError, ToAscObj,
};
use graph::{data::store, runtime::DeterministicHostError};
use graph::{prelude::serde_json, runtime::FromAscObj};
@@ -474,39 +472,6 @@ pub enum AscSubgraphEntityOp {
Delete,
}

#[derive(AscType)]
pub struct AscEntityTrigger {
pub entity_op: AscSubgraphEntityOp,
pub entity_type: AscPtr<AscString>,
pub entity: AscPtr<AscEntity>,
pub vid: i64,
}

impl ToAscObj<AscEntityTrigger> for EntitySourceOperation {
fn to_asc_obj<H: AscHeap + ?Sized>(
&self,
heap: &mut H,
gas: &GasCounter,
) -> Result<AscEntityTrigger, HostExportError> {
let entity_op = match self.entity_op {
EntityOperationKind::Create => AscSubgraphEntityOp::Create,
EntityOperationKind::Modify => AscSubgraphEntityOp::Modify,
EntityOperationKind::Delete => AscSubgraphEntityOp::Delete,
};

Ok(AscEntityTrigger {
entity_op,
entity_type: asc_new(heap, &self.entity_type.as_str(), gas)?,
entity: asc_new(heap, &self.entity.sorted_ref(), gas)?,
vid: self.vid,
})
}
}

impl AscIndexId for AscEntityTrigger {
const INDEX_ASC_TYPE_ID: IndexForAscTypeId = IndexForAscTypeId::AscEntityTrigger;
}

impl ToAscObj<AscEnum<YamlValueKind>> for serde_yaml::Value {
fn to_asc_obj<H: AscHeap + ?Sized>(
&self,
77 changes: 74 additions & 3 deletions store/test-store/tests/chain/ethereum/manifest.rs
Original file line number Diff line number Diff line change
@@ -47,9 +47,10 @@ specVersion: 1.3.0
";

const SOURCE_SUBGRAPH_SCHEMA: &str = "
type TestEntity @entity { id: ID! }
type User @entity { id: ID! }
type Profile @entity { id: ID! }
type TestEntity @entity(immutable: true) { id: ID! }
type MutableEntity @entity { id: ID! }
type User @entity(immutable: true) { id: ID! }
type Profile @entity(immutable: true) { id: ID! }

type TokenData @entity(timeseries: true) {
id: Int8!
@@ -1761,6 +1762,7 @@ specVersion: 1.3.0
let result = try_resolve_manifest(yaml, SPEC_VERSION_1_3_0).await;
assert!(result.is_err());
let err = result.unwrap_err();
println!("Error: {}", err);
assert!(err
.to_string()
.contains("Subgraph datasources cannot be used alongside onchain datasources"));
@@ -1857,3 +1859,72 @@ specVersion: 1.3.0
}
})
}

#[tokio::test]
async fn subgraph_ds_manifest_mutable_entities_should_fail() {
let yaml = "
schema:
file:
/: /ipfs/Qmschema
dataSources:
- name: SubgraphSource
kind: subgraph
entities:
- Gravatar
network: mainnet
source:
address: 'QmSource'
startBlock: 9562480
mapping:
apiVersion: 0.0.6
language: wasm/assemblyscript
entities:
- TestEntity
file:
/: /ipfs/Qmmapping
handlers:
- handler: handleEntity
entity: MutableEntity # This is a mutable entity and should fail
specVersion: 1.3.0
";

let result = try_resolve_manifest(yaml, SPEC_VERSION_1_3_0).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err
.to_string()
.contains("Entity MutableEntity is not immutable and cannot be used as a mapping entity"));
}

#[tokio::test]
async fn subgraph_ds_manifest_immutable_entities_should_succeed() {
let yaml = "
schema:
file:
/: /ipfs/Qmschema
dataSources:
- name: SubgraphSource
kind: subgraph
entities:
- Gravatar
network: mainnet
source:
address: 'QmSource'
startBlock: 9562480
mapping:
apiVersion: 0.0.6
language: wasm/assemblyscript
entities:
- TestEntity
file:
/: /ipfs/Qmmapping
handlers:
- handler: handleEntity
entity: User # This is an immutable entity and should succeed
specVersion: 1.3.0
";

let result = try_resolve_manifest(yaml, SPEC_VERSION_1_3_0).await;

assert!(result.is_ok());
}
4 changes: 2 additions & 2 deletions tests/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: '3'
services:
ipfs:
image: docker.io/ipfs/kubo:v0.17.0
image: docker.io/ipfs/kubo:v0.34.1
ports:
- '127.0.0.1:3001:5001'
postgres:
@@ -20,7 +20,7 @@ services:
POSTGRES_DB: graph-node
POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C"
anvil:
image: ghcr.io/foundry-rs/foundry:latest
image: ghcr.io/foundry-rs/foundry:stable
ports:
- '3021:8545'
command: "'anvil --host 0.0.0.0 --gas-limit 100000000000 --base-fee 1 --block-time 5 --mnemonic \"test test test test test test test test test test test junk\"'"
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
import { dataSource, EntityTrigger, log } from '@graphprotocol/graph-ts'
import { AggregatedData } from '../generated/schema'
import { SourceAData } from '../generated/subgraph-QmPWnNsD4m8T9EEF1ec5d8wetFxrMebggLj1efFHzdnZhx'
import { SourceBData } from '../generated/subgraph-Qma4Rk2D1w6mFiP15ZtHHx7eWkqFR426RWswreLiDanxej'
import { SourceAData } from '../generated/subgraph-QmYHp1bPEf7EoYBpEtJUpZv1uQHYQfWE4AhvR6frjB1Huj'
import { SourceBData } from '../generated/subgraph-QmYBEzastJi7bsa722ac78tnZa6xNnV9vvweerY4kVyJtq'

export function handleSourceAData(data: EntityTrigger<SourceAData>): void {
let aggregated = AggregatedData.load(data.data.id)
if (!aggregated) {
aggregated = new AggregatedData(data.data.id)
aggregated.sourceA = data.data.data
aggregated.first = 'sourceA'
} else {
aggregated.sourceA = data.data.data
}

// We know this handler will run first since its defined first in the manifest
// So we dont need to check if the Aggregated data exists
export function handleSourceAData(data: SourceAData): void {
let aggregated = new AggregatedData(data.id)
aggregated.sourceA = data.data
aggregated.first = 'sourceA'
aggregated.save()
}

export function handleSourceBData(data: EntityTrigger<SourceBData>): void {
let aggregated = AggregatedData.load(data.data.id)
export function handleSourceBData(data: SourceBData): void {
let aggregated = AggregatedData.load(data.id)
if (!aggregated) {
aggregated = new AggregatedData(data.data.id)
aggregated.sourceB = data.data.data
aggregated = new AggregatedData(data.id)
aggregated.sourceB = data.data
aggregated.first = 'sourceB'
} else {
aggregated.sourceB = data.data.data
aggregated.sourceB = data.data
}
aggregated.save()
}
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ dataSources:
name: SourceA
network: test
source:
address: 'QmPWnNsD4m8T9EEF1ec5d8wetFxrMebggLj1efFHzdnZhx'
address: 'QmYHp1bPEf7EoYBpEtJUpZv1uQHYQfWE4AhvR6frjB1Huj'
startBlock: 0
mapping:
apiVersion: 0.0.7
@@ -22,7 +22,7 @@ dataSources:
name: SourceB
network: test
source:
address: 'Qma4Rk2D1w6mFiP15ZtHHx7eWkqFR426RWswreLiDanxej'
address: 'QmYBEzastJi7bsa722ac78tnZa6xNnV9vvweerY4kVyJtq'
startBlock: 0
mapping:
apiVersion: 0.0.7
2 changes: 1 addition & 1 deletion tests/integration-tests/source-subgraph-a/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type SourceAData @entity {
type SourceAData @entity(immutable: true) {
id: ID!
data: String!
blockNumber: BigInt!
2 changes: 1 addition & 1 deletion tests/integration-tests/source-subgraph-b/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type SourceBData @entity {
type SourceBData @entity(immutable: true) {
id: ID!
data: String!
blockNumber: BigInt!
5 changes: 2 additions & 3 deletions tests/integration-tests/source-subgraph/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
type Block @entity {
type Block @entity(immutable: true) {
id: ID!
number: BigInt!
hash: Bytes!
testMessage: String
}

type Block2 @entity {
type Block2 @entity(immutable: true) {
id: ID!
number: BigInt!
hash: Bytes!
34 changes: 1 addition & 33 deletions tests/integration-tests/source-subgraph/src/mapping.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ethereum, log, store } from '@graphprotocol/graph-ts';
import { Block, Block2 } from '../generated/schema';
import { BigInt } from '@graphprotocol/graph-ts';

export function handleBlock(block: ethereum.Block): void {
log.info('handleBlock {}', [block.number.toString()]);
@@ -21,37 +20,6 @@ export function handleBlock(block: ethereum.Block): void {
let blockEntity3 = new Block2(id3);
blockEntity3.number = block.number;
blockEntity3.hash = block.hash;
blockEntity3.testMessage = block.number.toString().concat('-message');
blockEntity3.save();

if (block.number.equals(BigInt.fromI32(1))) {
let id = 'TEST';
let entity = new Block(id);
entity.number = block.number;
entity.hash = block.hash;
entity.testMessage = 'Created at block 1';
log.info('Created entity at block 1', []);
entity.save();
}

if (block.number.equals(BigInt.fromI32(2))) {
let id = 'TEST';
let blockEntity1 = Block.load(id);
if (blockEntity1) {
// Update the block entity
blockEntity1.testMessage = 'Updated at block 2';
log.info('Updated entity at block 2', []);
blockEntity1.save();
}
}

if (block.number.equals(BigInt.fromI32(3))) {
let id = 'TEST';
let blockEntity1 = Block.load(id);
if (blockEntity1) {
blockEntity1.testMessage = 'Deleted at block 3';
log.info('Deleted entity at block 3', []);
blockEntity1.save();
store.remove('Block', id);
}
}
}
36 changes: 18 additions & 18 deletions tests/integration-tests/subgraph-data-sources/src/mapping.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { Entity, log, store, BigInt, EntityTrigger, EntityOp } from '@graphprotocol/graph-ts';
import { Block } from '../generated/subgraph-QmVz1Pt7NhgCkz4gfavmNrMhojnMT9hW81QDqVjy56ZMUP';
import { log, store } from '@graphprotocol/graph-ts';
import { Block, Block2 } from '../generated/subgraph-QmWi3H11QFE2PiWx6WcQkZYZdA5UasaBptUJqGn54MFux5';
import { MirrorBlock } from '../generated/schema';

export function handleEntity(trigger: EntityTrigger<Block>): void {
let blockEntity = trigger.data;
let id = blockEntity.id;
export function handleEntity(block: Block): void {
let id = block.id;

if (trigger.operation === EntityOp.Remove) {
log.info('Removing block entity with id: {}', [id]);
store.remove('MirrorBlock', id);
return;
}
let blockEntity = loadOrCreateMirrorBlock(id);
blockEntity.number = block.number;
blockEntity.hash = block.hash;

let block = loadOrCreateMirrorBlock(id);
block.number = blockEntity.number;
block.hash = blockEntity.hash;

if (blockEntity.testMessage) {
block.testMessage = blockEntity.testMessage;
}
blockEntity.save();
}

export function handleEntity2(block: Block2): void {
let id = block.id;

let blockEntity = loadOrCreateMirrorBlock(id);
blockEntity.number = block.number;
blockEntity.hash = block.hash;
blockEntity.testMessage = block.testMessage;

block.save();
blockEntity.save();
}

export function loadOrCreateMirrorBlock(id: string): MirrorBlock {
Loading
Oops, something went wrong.
Loading
Oops, something went wrong.