Add idl-compare script to check for binary incompatibilities (#498)
This commit is contained in:
parent
6ee3cb1e19
commit
9a8cb3c7a9
|
@ -112,6 +112,31 @@ jobs:
|
|||
name: raw-test-bpf
|
||||
path: raw-test-bpf.log
|
||||
|
||||
idl:
|
||||
name: IDL Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: main
|
||||
path: main
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Check
|
||||
run: yarn ts-node ts/client/scripts/idl-compare.ts main/mango_v4.json mango_v4.json
|
||||
|
||||
sca:
|
||||
name: Dependency Scan
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
@ -0,0 +1,195 @@
|
|||
import { Idl } from '@coral-xyz/anchor';
|
||||
import {
|
||||
IdlEnumVariant,
|
||||
IdlField,
|
||||
IdlType,
|
||||
IdlTypeDef,
|
||||
} from '@coral-xyz/anchor/dist/cjs/idl';
|
||||
import fs from 'fs';
|
||||
|
||||
function main(): void {
|
||||
let hasError = false;
|
||||
|
||||
const oldIdl = JSON.parse(fs.readFileSync(process.argv[2], 'utf-8')) as Idl;
|
||||
const newIdl = JSON.parse(fs.readFileSync(process.argv[3], 'utf-8')) as Idl;
|
||||
|
||||
// Old instructions still exist
|
||||
for (const oldIx of oldIdl.instructions) {
|
||||
if (!newIdl.instructions.find((x) => x.name == oldIx.name)) {
|
||||
console.log(`Error: instruction '${oldIx.name}' was removed`);
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const oldAcc of oldIdl.accounts ?? []) {
|
||||
const newAcc = newIdl.accounts?.find((x) => x.name == oldAcc.name);
|
||||
|
||||
// Old accounts still exist
|
||||
if (!newAcc) {
|
||||
console.log(`Error: account '${oldAcc.name}' was removed`);
|
||||
hasError = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const oldSize = accountSize(oldIdl, oldAcc);
|
||||
const newSize = accountSize(newIdl, newAcc);
|
||||
if (oldSize != newSize) {
|
||||
console.log(`Error: account '${oldAcc.name}' has changed size`);
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
// Data offset matches for each account field with the same name
|
||||
const newFields = newAcc.type.fields;
|
||||
const oldFields = oldAcc.type.fields;
|
||||
for (const oldField of oldFields) {
|
||||
const newField = newFields.find((x) => x.name == oldField.name);
|
||||
|
||||
if (
|
||||
oldField.name.startsWith('reserved') ||
|
||||
oldField.name.startsWith('padding')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Old fields may be renamed / deprecated
|
||||
if (!newField) {
|
||||
console.log(
|
||||
`Warning: account field '${oldAcc.name}.${oldField.name}' was removed`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fields may not change size
|
||||
const oldSize = typeSize(oldIdl, oldField.type);
|
||||
const newSize = typeSize(newIdl, newField.type);
|
||||
if (oldSize != newSize) {
|
||||
console.log(
|
||||
`Error: account field '${oldAcc.name}.${oldField.name}' has changed size`,
|
||||
);
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
// Fields may not change offset
|
||||
const oldOffset = fieldOffset(oldFields, oldField, oldIdl);
|
||||
const newOffset = fieldOffset(newFields, newField, newIdl);
|
||||
if (oldOffset != newOffset) {
|
||||
console.log(
|
||||
`Error: account field '${oldAcc.name}.${oldField.name}' has changed offset`,
|
||||
);
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(hasError ? 1 : 0);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
function fieldOffset(fields: IdlField[], field: IdlField, idl: Idl): number {
|
||||
let offset = 0;
|
||||
for (const f of fields) {
|
||||
if (f.name == field.name) {
|
||||
break;
|
||||
}
|
||||
offset += typeSize(idl, f.type);
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
//
|
||||
// The following code is essentially copied from anchor's common.ts
|
||||
//
|
||||
|
||||
export function accountSize(idl: Idl, idlAccount: IdlTypeDef): number {
|
||||
if (idlAccount.type.kind === 'enum') {
|
||||
const variantSizes = idlAccount.type.variants.map(
|
||||
(variant: IdlEnumVariant) => {
|
||||
if (variant.fields === undefined) {
|
||||
return 0;
|
||||
}
|
||||
return variant.fields
|
||||
.map((f: IdlField | IdlType) => {
|
||||
if (!(typeof f === 'object' && 'name' in f)) {
|
||||
throw new Error('Tuple enum variants not yet implemented.');
|
||||
}
|
||||
return typeSize(idl, f.type);
|
||||
})
|
||||
.reduce((a: number, b: number) => a + b);
|
||||
},
|
||||
);
|
||||
return Math.max(...variantSizes) + 1;
|
||||
}
|
||||
if (idlAccount.type.fields === undefined) {
|
||||
return 0;
|
||||
}
|
||||
return idlAccount.type.fields
|
||||
.map((f) => typeSize(idl, f.type))
|
||||
.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
function typeSize(idl: Idl, ty: IdlType): number {
|
||||
switch (ty) {
|
||||
case 'bool':
|
||||
return 1;
|
||||
case 'u8':
|
||||
return 1;
|
||||
case 'i8':
|
||||
return 1;
|
||||
case 'i16':
|
||||
return 2;
|
||||
case 'u16':
|
||||
return 2;
|
||||
case 'u32':
|
||||
return 4;
|
||||
case 'i32':
|
||||
return 4;
|
||||
case 'f32':
|
||||
return 4;
|
||||
case 'u64':
|
||||
return 8;
|
||||
case 'i64':
|
||||
return 8;
|
||||
case 'f64':
|
||||
return 8;
|
||||
case 'u128':
|
||||
return 16;
|
||||
case 'i128':
|
||||
return 16;
|
||||
case 'u256':
|
||||
return 32;
|
||||
case 'i256':
|
||||
return 32;
|
||||
case 'bytes':
|
||||
return 1;
|
||||
case 'string':
|
||||
return 1;
|
||||
case 'publicKey':
|
||||
return 32;
|
||||
default:
|
||||
if ('vec' in ty) {
|
||||
return 1;
|
||||
}
|
||||
if ('option' in ty) {
|
||||
return 1 + typeSize(idl, ty.option);
|
||||
}
|
||||
if ('coption' in ty) {
|
||||
return 4 + typeSize(idl, ty.coption);
|
||||
}
|
||||
if ('defined' in ty) {
|
||||
const filtered = idl.types?.filter((t) => t.name === ty.defined) ?? [];
|
||||
if (filtered.length !== 1) {
|
||||
throw new Error(`Type not found: ${JSON.stringify(ty)}`);
|
||||
}
|
||||
const typeDef = filtered[0];
|
||||
|
||||
return accountSize(idl, typeDef);
|
||||
}
|
||||
if ('array' in ty) {
|
||||
const arrayTy = ty.array[0];
|
||||
const arraySize = ty.array[1];
|
||||
return typeSize(idl, arrayTy) * arraySize;
|
||||
}
|
||||
throw new Error(`Invalid type ${JSON.stringify(ty)}`);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue