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
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
11 changes: 10 additions & 1 deletion graph/src/data_source/subgraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Expand Down
12 changes: 1 addition & 11 deletions runtime/wasm/src/module/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}

Expand Down
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};
Expand Down Expand Up @@ -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,
Expand Down
77 changes: 74 additions & 3 deletions store/test-store/tests/chain/ethereum/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -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:
Expand All @@ -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\"'"
Expand Down
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
Expand Up @@ -6,7 +6,7 @@ dataSources:
name: SourceA
network: test
source:
address: 'QmPWnNsD4m8T9EEF1ec5d8wetFxrMebggLj1efFHzdnZhx'
address: 'QmYHp1bPEf7EoYBpEtJUpZv1uQHYQfWE4AhvR6frjB1Huj'
startBlock: 0
mapping:
apiVersion: 0.0.7
Expand All @@ -22,7 +22,7 @@ dataSources:
name: SourceB
network: test
source:
address: 'Qma4Rk2D1w6mFiP15ZtHHx7eWkqFR426RWswreLiDanxej'
address: 'QmYBEzastJi7bsa722ac78tnZa6xNnV9vvweerY4kVyJtq'
startBlock: 0
mapping:
apiVersion: 0.0.7
Expand Down
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!
Expand Down
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!
Expand Down
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!
Expand Down
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()]);
Expand All @@ -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 {
Expand Down
Loading
Loading