diff --git a/packages/neo-one-client-common/src/common.ts b/packages/neo-one-client-common/src/common.ts index f79cffc05e..9cd482f450 100644 --- a/packages/neo-one-client-common/src/common.ts +++ b/packages/neo-one-client-common/src/common.ts @@ -221,15 +221,21 @@ const TEN_THOUSAND_FIXED8 = fixed8FromDecimal(10000); const ONE_HUNDRED_MILLION_FIXED8 = fixed8FromDecimal(100000000); const nativeScriptHashes = { - GAS: '0x668e0c1f9d7b70a99dd9e06eadd4c784d641afbc', - NEO: '0xde5f57d430d3dece511cf975a8d37848cb9e0525', - Policy: '0xce06595079cd69583126dbfd1d2e25cca74cffe9', + Management: '0xcd97b70d82d69adfcd9165374109419fade8d6ab', + NEO: '0x0a46e2e37c9987f570b4af253fb77e7eef0f72b6', + GAS: '0xa6a6c15dcdc9b997dac448b6926522d22efeedfb', + Policy: '0xdde31084c0fdbebc7f5ed5f53a38905305ccee14', + Oracle: '0xb1c37d5847c2ae36bdde31d0cc833a7ad9667f8f', + Designation: '0xc0073f4c7069bf38995780c9da065f9b3949ea7a', }; const nativeHashes = { + Management: hexToUInt160(nativeScriptHashes.Management), GAS: hexToUInt160(nativeScriptHashes.GAS), NEO: hexToUInt160(nativeScriptHashes.NEO), Policy: hexToUInt160(nativeScriptHashes.Policy), + Oracle: hexToUInt160(nativeScriptHashes.Oracle), + Designation: hexToUInt160(nativeScriptHashes.Designation), }; export const common = { diff --git a/packages/neo-one-client-common/src/crypto.ts b/packages/neo-one-client-common/src/crypto.ts index f2cd912838..fdb684dd72 100644 --- a/packages/neo-one-client-common/src/crypto.ts +++ b/packages/neo-one-client-common/src/crypto.ts @@ -187,6 +187,15 @@ const createPrivateKey = (): PrivateKey => common.bufferToPrivateKey(randomBytes const toScriptHash = hash160; +const getContractHash = (sender: UInt160, script: Buffer) => { + const builder = new ScriptBuilder(); + builder.emitOp('ABORT'); + builder.emitPushUInt160(sender); + builder.emitPush(script); + + return toScriptHash(builder.build()); +}; + // Takes various formats and converts to standard ECPoint const toECPoint = (publicKey: Buffer): ECPoint => toECPointFromKeyPair(ec().keyFromPublic(publicKey)); @@ -891,6 +900,7 @@ export const crypto = { verify, privateKeyToPublicKey, toScriptHash, + getContractHash, toECPoint, createKeyPair, scriptHashToAddress, diff --git a/packages/neo-one-client-common/src/errors.ts b/packages/neo-one-client-common/src/errors.ts index 4fcfdbe844..d1e3d26937 100644 --- a/packages/neo-one-client-common/src/errors.ts +++ b/packages/neo-one-client-common/src/errors.ts @@ -95,7 +95,12 @@ export const InvalidAttributeTypeJSONError = makeErrorWithCode( ); export const InvalidAttributeTypeError = makeErrorWithCode( 'INVALID_ATTRIBUTE_TYPE', - (transactionAttributeType: number) => `Expected transaction type, found: ${transactionAttributeType.toString(16)}`, + (transactionAttributeType: number) => + `Expected transaction attribute type, found: ${transactionAttributeType.toString(16)}`, +); +export const InvalidOracleResponseCodeError = makeErrorWithCode( + 'INVALID_ORACLE_RESPONSE_CODE', + (value: number) => `Expected oracle response code, found: ${value.toString()}`, ); export const InvalidAttributeUsageError = makeErrorWithCode( 'INVALID_ATTRIBUTE_USAGE', diff --git a/packages/neo-one-client-common/src/models/transaction/TransactionModel.ts b/packages/neo-one-client-common/src/models/transaction/TransactionModel.ts index 2e60cfa238..7c7f4a1fa8 100644 --- a/packages/neo-one-client-common/src/models/transaction/TransactionModel.ts +++ b/packages/neo-one-client-common/src/models/transaction/TransactionModel.ts @@ -71,6 +71,14 @@ export class TransactionModel< this.networkFee = networkFee; } + public getAttributes(isAttr: (attr: TAttribute) => attr is T): readonly T[] { + return this.attributes.filter(isAttr); + } + + public getAttribute(isAttr: (attr: TAttribute) => attr is T): T | undefined { + return this.getAttributes(isAttr)[0]; + } + public getScriptHashesForVerifying(): readonly UInt160[] { return this.signers.map((signer) => signer.account); } diff --git a/packages/neo-one-client-common/src/models/transaction/attribute/AttributeModel.ts b/packages/neo-one-client-common/src/models/transaction/attribute/AttributeModel.ts index c3e007ac6f..88cb26e419 100644 --- a/packages/neo-one-client-common/src/models/transaction/attribute/AttributeModel.ts +++ b/packages/neo-one-client-common/src/models/transaction/attribute/AttributeModel.ts @@ -1,3 +1,4 @@ import { HighPriorityAttributeModel } from './HighPriorityAttributeModel'; +import { OracleResponseModel } from './OracleResponseModel'; -export type AttributeModel = HighPriorityAttributeModel; +export type AttributeModel = HighPriorityAttributeModel | OracleResponseModel; diff --git a/packages/neo-one-client-common/src/models/transaction/attribute/AttributeTypeModel.ts b/packages/neo-one-client-common/src/models/transaction/attribute/AttributeTypeModel.ts index 2e267729f8..1afe84f14d 100644 --- a/packages/neo-one-client-common/src/models/transaction/attribute/AttributeTypeModel.ts +++ b/packages/neo-one-client-common/src/models/transaction/attribute/AttributeTypeModel.ts @@ -2,7 +2,8 @@ import { InvalidAttributeTypeError, InvalidAttributeTypeJSONError } from '../../ import { AttributeTypeJSON } from '../../types'; export enum AttributeTypeModel { - HighPriority = 1, + HighPriority = 0x01, + OracleResponse = 0x11, } const isAttributeType = (value: number): value is AttributeTypeModel => diff --git a/packages/neo-one-client-common/src/models/transaction/attribute/OracleResponseCode.ts b/packages/neo-one-client-common/src/models/transaction/attribute/OracleResponseCode.ts new file mode 100644 index 0000000000..0d67158b02 --- /dev/null +++ b/packages/neo-one-client-common/src/models/transaction/attribute/OracleResponseCode.ts @@ -0,0 +1,26 @@ +import { InvalidOracleResponseCodeError } from '../../../errors'; + +export enum OracleResponseCode { + Success = 0x00, + + ConsensusUnreachable = 0x10, + NotFound = 0x12, + Timeout = 0x14, + Forbidden = 0x16, + ResponseTooLarge = 0x18, + InsufficientFunds = 0x1a, + + Error = 0xff, +} + +const isOracleResponseCode = (value: number): value is OracleResponseCode => + // tslint:disable-next-line: strict-type-predicates + OracleResponseCode[value] !== undefined; + +export const assertOracleResponseCode = (value: number): OracleResponseCode => { + if (isOracleResponseCode(value)) { + return value; + } + + throw new InvalidOracleResponseCodeError(value); +}; diff --git a/packages/neo-one-client-common/src/models/transaction/attribute/OracleResponseModel.ts b/packages/neo-one-client-common/src/models/transaction/attribute/OracleResponseModel.ts new file mode 100644 index 0000000000..4695399ba3 --- /dev/null +++ b/packages/neo-one-client-common/src/models/transaction/attribute/OracleResponseModel.ts @@ -0,0 +1,37 @@ +import { BN } from 'bn.js'; +import { BinaryWriter } from '../../../BinaryWriter'; +import { IOHelper } from '../../../IOHelper'; +import { AttributeBaseModel } from './AttributeBaseModel'; +import { AttributeTypeModel } from './AttributeTypeModel'; +import { OracleResponseCode } from './OracleResponseCode'; + +export interface OracleResponseModelAdd { + readonly id: BN; + readonly code: OracleResponseCode; + readonly result: Buffer; +} + +export class OracleResponseModel extends AttributeBaseModel { + public readonly type = AttributeTypeModel.OracleResponse; + public readonly allowMultiple = false; + public readonly id: BN; + public readonly code: OracleResponseCode; + public readonly result: Buffer; + + public constructor({ id, code, result }: OracleResponseModelAdd) { + super(); + this.id = id; + this.code = code; + this.result = result; + } + + protected serializeWithoutTypeBase(writer: BinaryWriter) { + writer.writeUInt64LE(this.id); + writer.writeUInt8(this.code); + writer.writeVarBytesLE(this.result); + } + + protected sizeExclusive(): number { + return IOHelper.sizeOfUInt64LE + IOHelper.sizeOfUInt8 + IOHelper.sizeOfVarBytesLE(this.result); + } +} diff --git a/packages/neo-one-client-common/src/models/transaction/attribute/index.ts b/packages/neo-one-client-common/src/models/transaction/attribute/index.ts index 09d44f1986..9e1778f2d4 100644 --- a/packages/neo-one-client-common/src/models/transaction/attribute/index.ts +++ b/packages/neo-one-client-common/src/models/transaction/attribute/index.ts @@ -2,3 +2,5 @@ export * from './AttributeBaseModel'; export * from './AttributeModel'; export * from './AttributeTypeModel'; export * from './HighPriorityAttributeModel'; +export * from './OracleResponseCode'; +export * from './OracleResponseModel'; diff --git a/packages/neo-one-client-common/src/models/types.ts b/packages/neo-one-client-common/src/models/types.ts index 05fdfe479a..146391fda3 100644 --- a/packages/neo-one-client-common/src/models/types.ts +++ b/packages/neo-one-client-common/src/models/types.ts @@ -3,7 +3,7 @@ import { UInt256Hex } from '../common'; import { UserAccount } from '../types'; import { ContractParameterTypeModel } from './ContractParameterTypeModel'; import { StorageFlagsModel } from './StorageFlagsModel'; -import { AttributeTypeModel } from './transaction/attribute/AttributeTypeModel'; +import { AttributeTypeModel } from './transaction'; import { TriggerType, TriggerTypeJSON } from './trigger'; import { VerifyResultModel } from './VerifyResultModel'; import { VMState, VMStateJSON } from './vm'; @@ -196,10 +196,23 @@ export interface SignerJSON { readonly allowedgroups?: readonly string[]; } -export interface AttributeJSON { +export interface AttributeJSONBase { readonly type: AttributeTypeJSON; } +export interface HighPriorityAttributeJSON extends AttributeJSONBase { + readonly type: 'HighPriority'; +} + +export interface OracleResponseJSON extends AttributeJSONBase { + readonly type: 'OracleResponse'; + readonly id: string; + readonly code: number; + readonly result: string; +} + +export type AttributeJSON = HighPriorityAttributeJSON | OracleResponseJSON; + export type AttributeTypeJSON = keyof typeof AttributeTypeModel; export type VerifyResultJSON = keyof typeof VerifyResultModel; @@ -389,6 +402,7 @@ export interface ContractParameterDefinitionJSON { export interface ContractJSON { readonly id: number; + readonly updatecounter: number; readonly hash: string; readonly script: string; readonly manifest: ContractManifestJSON; diff --git a/packages/neo-one-client-common/src/prices.ts b/packages/neo-one-client-common/src/prices.ts index 4c4c94f3c7..9c3a77afa2 100644 --- a/packages/neo-one-client-common/src/prices.ts +++ b/packages/neo-one-client-common/src/prices.ts @@ -199,3 +199,17 @@ export const getOpCodePrice = (value: Op): BigNumber => { return fee; }; + +export const signatureContractCost = getOpCodePrice(Op.PUSHDATA1) + .multipliedBy(2) + .plus(getOpCodePrice(Op.PUSHNULL)) + .plus(getOpCodePrice(Op.SYSCALL)) + .plus(ECDsaVerifyPrice); + +export const multiSignatureContractCost = (m: number, n: number) => + getOpCodePrice(Op.PUSHDATA1) + .multipliedBy(m + n) + .plus(getOpCodePrice(Op.PUSHINT8).multipliedBy(2)) + .plus(getOpCodePrice(Op.PUSHNULL)) + .plus(getOpCodePrice(Op.SYSCALL)) + .plus(ECDsaVerifyPrice.multipliedBy(n)); diff --git a/packages/neo-one-client-common/src/types.ts b/packages/neo-one-client-common/src/types.ts index 0d5f787018..a7b7eea060 100644 --- a/packages/neo-one-client-common/src/types.ts +++ b/packages/neo-one-client-common/src/types.ts @@ -9,6 +9,7 @@ import { AccountContract, AttributeTypeModel, NotificationJSON, + OracleResponseCode, StackItemJSON, TriggerTypeJSON, VerifyResultModel, @@ -73,21 +74,28 @@ export type NetworkType = 'main' | 'test' | string; * @see Attribute */ export interface AttributeBase { + /** + * `type` specifies the `Attribute` type + */ readonly type: AttributeTypeModel; } /** * `Attribute` whose transaction is "high priority". */ export interface HighPriorityAttribute extends AttributeBase { - /** - * `type` specifies the `Attribute` type - */ readonly type: AttributeTypeModel.HighPriority; } + +export interface OracleResponse extends AttributeBase { + readonly type: AttributeTypeModel.OracleResponse; + readonly id: BigNumber; + readonly code: OracleResponseCode; + readonly result: BufferString; +} /** * `Attribute`s are used to store additional data on `Transaction`s. */ -export type Attribute = HighPriorityAttribute; +export type Attribute = HighPriorityAttribute | OracleResponse; export type WitnessScope = | 'None' @@ -429,6 +437,10 @@ export interface Transfer { * Destination address. */ readonly to: AddressString; + /** + * Additional data to be attached to the transaction. Typed as `any` but should be used cautiously since it will need to be converted. + */ + readonly data?: any; } /** diff --git a/packages/neo-one-client-core/src/provider/JSONRPCClient.ts b/packages/neo-one-client-core/src/provider/JSONRPCClient.ts index c7f9b4f730..746b8bad3c 100644 --- a/packages/neo-one-client-core/src/provider/JSONRPCClient.ts +++ b/packages/neo-one-client-core/src/provider/JSONRPCClient.ts @@ -81,6 +81,10 @@ export class JSONRPCClient { return this.withInstance(async (provider) => provider.request({ method: 'getfeeperbyte' })); } + public async getExecFeeFactor(): Promise { + return this.withInstance(async (provider) => provider.request({ method: 'getexecfeefactor' })); + } + public async getContract(address: AddressString): Promise { return this.withInstance(async (provider) => provider.request({ diff --git a/packages/neo-one-client-core/src/provider/NEOONEDataProvider.ts b/packages/neo-one-client-core/src/provider/NEOONEDataProvider.ts index c0ed5281b4..0f3fb8e128 100644 --- a/packages/neo-one-client-core/src/provider/NEOONEDataProvider.ts +++ b/packages/neo-one-client-core/src/provider/NEOONEDataProvider.ts @@ -5,6 +5,7 @@ import { ApplicationLogJSON, Attribute, AttributeJSON, + AttributeTypeModel, Block, BlockJSON, ConfirmedTransaction, @@ -27,7 +28,6 @@ import { ContractPermission, ContractPermissionJSON, DeveloperProvider, - FeelessTransactionModel, GetOptions, Hash256String, IterOptions, @@ -35,6 +35,7 @@ import { NetworkSettings, NetworkSettingsJSON, NetworkType, + OracleResponseJSON, Peer, PrivateNetworkSettings, RawApplicationLogData, @@ -54,9 +55,9 @@ import { TransactionModel, TransactionReceipt, TransactionReceiptJSON, + UInt160Hex, VerifyResultJSON, VerifyResultModel, - UInt160Hex, } from '@neo-one/client-common'; import { utils as commonUtils } from '@neo-one/utils'; import { AsyncIterableX } from '@reactivex/ix-es2015-cjs/asynciterable/asynciterablex'; @@ -150,6 +151,10 @@ export class NEOONEDataProvider implements DeveloperProvider { return new BigNumber(feePerByte); } + public async getExecFeeFactor(): Promise { + return this.mutableClient.getExecFeeFactor(); + } + public async getVerificationCost( hash: AddressString, transaction: TransactionModel, @@ -356,9 +361,29 @@ export class NEOONEDataProvider implements DeveloperProvider { } private convertAttributes(attributes: readonly AttributeJSON[]): readonly Attribute[] { - return attributes.map((attribute) => ({ - type: toAttributeType(attribute.type), - })); + return attributes.map(this.convertAttribute); + } + + private convertAttribute(attribute: AttributeJSON): Attribute { + const type = toAttributeType(attribute.type); + switch (type) { + case AttributeTypeModel.HighPriority: + return { + type, + }; + case AttributeTypeModel.OracleResponse: + // tslint:disable-next-line: no-any we know this is true but TS is being mean + const oracleJSON = attribute as OracleResponseJSON; + + return { + type, + id: new BigNumber(oracleJSON.id), + code: oracleJSON.code, + result: oracleJSON.result, + }; + default: + throw new Error(); + } } private convertContract(contract: ContractJSON): Contract { diff --git a/packages/neo-one-client-core/src/provider/NEOONEProvider.ts b/packages/neo-one-client-core/src/provider/NEOONEProvider.ts index b2bd704da6..a49661024b 100644 --- a/packages/neo-one-client-core/src/provider/NEOONEProvider.ts +++ b/packages/neo-one-client-core/src/provider/NEOONEProvider.ts @@ -2,7 +2,6 @@ import { Account, AddressString, Block, - FeelessTransactionModel, GetOptions, Hash256String, IterOptions, @@ -85,6 +84,10 @@ export class NEOONEProvider implements Provider { return this.getProvider(network).getFeePerByte(); } + public async getExecFeeFactor(network: NetworkType): Promise { + return this.getProvider(network).getExecFeeFactor(); + } + public async getVerificationCost( network: NetworkType, hash: UInt160Hex, diff --git a/packages/neo-one-client-core/src/user/LocalUserAccountProvider.ts b/packages/neo-one-client-core/src/user/LocalUserAccountProvider.ts index 0ff705f71f..92cebe99b1 100644 --- a/packages/neo-one-client-core/src/user/LocalUserAccountProvider.ts +++ b/packages/neo-one-client-core/src/user/LocalUserAccountProvider.ts @@ -12,7 +12,6 @@ import { NetworkType, Op, Param, - RawAction, RelayTransactionResult, ScriptBuilder, ScriptBuilderParam, @@ -33,7 +32,6 @@ import { WitnessScopeModel, } from '@neo-one/client-common'; import { processActionsAndMessage } from '@neo-one/client-switch'; -import { utils as commonUtils } from '@neo-one/utils'; import BigNumber from 'bignumber.js'; import { Observable } from 'rxjs'; import { InsufficientNetworkFeeError, InvokeError, UnknownAccountError } from '../errors'; @@ -171,6 +169,11 @@ export class LocalUserAccountProvider Promise; readonly getBlockCount: (network: NetworkType) => Promise; readonly getFeePerByte: (network: NetworkType) => Promise; + readonly getExecFeeFactor: (network: NetworkType) => Promise; readonly getTransaction: (network: NetworkType, hash: Hash256String) => Promise; readonly iterBlocks: (network: NetworkType, options?: IterOptions) => AsyncIterable; readonly getAccount: (network: NetworkType, address: AddressString) => Promise; diff --git a/packages/neo-one-client-core/src/user/converters/attribute.ts b/packages/neo-one-client-core/src/user/converters/attribute.ts index 46fceb2ff8..cc66ed3c7e 100644 --- a/packages/neo-one-client-core/src/user/converters/attribute.ts +++ b/packages/neo-one-client-core/src/user/converters/attribute.ts @@ -1,8 +1,23 @@ -import { Attribute, AttributeModel, HighPriorityAttributeModel } from '@neo-one/client-common'; +import { + Attribute, + AttributeModel, + AttributeTypeModel, + HighPriorityAttributeModel, + OracleResponseModel, +} from '@neo-one/client-common'; +import { BN } from 'bn.js'; export const attribute = (attrib: Attribute): AttributeModel => { switch (attrib.type) { - default: + case AttributeTypeModel.HighPriority: return new HighPriorityAttributeModel(); + case AttributeTypeModel.OracleResponse: + return new OracleResponseModel({ + id: new BN(attrib.id.toString()), + code: attrib.code, + result: Buffer.from(attrib.result, 'hex'), + }); + default: + throw new Error('for ts'); } }; diff --git a/packages/neo-one-client-full-common/src/models/ContractStateModel.ts b/packages/neo-one-client-full-common/src/models/ContractStateModel.ts index ee418e1f6d..b2d5c6e11b 100644 --- a/packages/neo-one-client-full-common/src/models/ContractStateModel.ts +++ b/packages/neo-one-client-full-common/src/models/ContractStateModel.ts @@ -1,28 +1,26 @@ -import { BinaryWriter, createSerializeWire, SerializableWire, SerializeWire } from '@neo-one/client-common'; import { ContractManifestModel } from './manifest'; +import { UInt160 } from '@neo-one/client-common'; export interface ContractStateModelAdd { readonly id: number; + readonly updateCounter: number; + readonly hash: UInt160; readonly script: Buffer; readonly manifest: TContractManifest; } -export class ContractStateModel - implements SerializableWire { +export class ContractStateModel { public readonly id: number; + public readonly updateCounter: number; + public readonly hash: UInt160; public readonly script: Buffer; public readonly manifest: TContractManifest; - public readonly serializeWire: SerializeWire = createSerializeWire(this.serializeWireBase.bind(this)); - public constructor({ script, manifest, id }: ContractStateModelAdd) { + public constructor({ script, hash, manifest, id, updateCounter }: ContractStateModelAdd) { this.id = id; + this.updateCounter = updateCounter; + this.hash = hash; this.script = script; this.manifest = manifest; } - - public serializeWireBase(writer: BinaryWriter): void { - writer.writeUInt32LE(this.id); - writer.writeVarBytesLE(this.script); - this.manifest.serializeWireBase(writer); - } } diff --git a/packages/neo-one-node-blockchain/src/Blockchain.ts b/packages/neo-one-node-blockchain/src/Blockchain.ts index 8c5e447830..6f429f100a 100644 --- a/packages/neo-one-node-blockchain/src/Blockchain.ts +++ b/packages/neo-one-node-blockchain/src/Blockchain.ts @@ -31,7 +31,6 @@ import { Storage, Transaction, TransactionVerificationContext, - VerifyConsensusPayloadOptions, VerifyOptions, VM, Witness, @@ -51,7 +50,7 @@ import { getNep5UpdateOptions } from './getNep5UpdateOptions'; import { HeaderIndexCache } from './HeaderIndexCache'; import { PersistingBlockchain } from './PersistingBlockchain'; import { utils } from './utils'; -import { verifyWitnesses } from './verify'; +import { verifyWitness, verifyWitnesses } from './verify'; const logger = createChild(nodeLogger, { service: 'blockchain' }); @@ -165,6 +164,7 @@ export class Blockchain { public readonly deserializeWireContext: DeserializeWireContext; + public readonly verifyWitness = verifyWitness; public readonly verifyWitnesses = verifyWitnesses; public readonly settings: BlockchainSettings; public readonly onPersistNativeContractScript: Buffer; @@ -205,7 +205,11 @@ export class Blockchain { }; this.mutableCurrentBlock = options.currentBlock; this.onPersist = - options.onPersist === undefined ? () => Promise.resolve(this.vm.updateSnapshots()) : options.onPersist; + options.onPersist === undefined + ? () => { + this.vm.updateSnapshots(); + } + : options.onPersist; this.start(); } @@ -243,15 +247,7 @@ export class Blockchain { storage: this.storage, native: this.native, verifyWitnesses: this.verifyWitnesses, - }; - } - - public get verifyConsensusPayloadOptions(): VerifyConsensusPayloadOptions { - return { - vm: this.vm, - storage: this.storage, - native: this.native, - verifyWitnesses: this.verifyWitnesses, + verifyWitness: this.verifyWitness, height: this.currentBlockIndex, }; } @@ -280,10 +276,6 @@ export class Blockchain { return this.storage.transactions; } - public get contracts() { - return this.storage.contracts; - } - public get storages() { return this.storage.storages; } @@ -300,10 +292,6 @@ export class Blockchain { return this.storage.headerHashIndex; } - public get contractID() { - return this.storage.contractID; - } - // public get consensusState() { // return this.storage.consensusState; // } @@ -363,20 +351,11 @@ export class Blockchain { return VerifyResultModel.AlreadyExists; } - // TODO: to save some compute time we could keep a local cache of the current blocks return values - // from native contract calls and pass those in, instead of passing the whole native container. - const verifyOptions = { - native: this.native, - vm: this.vm, - storage: this.storage, - verifyWitnesses: this.verifyWitnesses, - }; - - return transaction.verify(verifyOptions, context); + return transaction.verify(this.verifyOptions, context); } public async verifyConsensusPayload(payload: ConsensusPayload) { - const verification = await payload.verify(this.verifyConsensusPayloadOptions); + const verification = await payload.verify(this.verifyOptions); if (!verification) { throw new ConsensusPayloadVerifyError(payload.hashHex); } @@ -447,7 +426,7 @@ export class Blockchain { } public async getValidators(): Promise { - return this.native.NEO.getValidators(this.storage); + return this.native.NEO.computeNextBlockValidators(this.storage); } public async getNextBlockValidators(): Promise { @@ -498,7 +477,7 @@ export class Blockchain { } public async getVerificationCost(contractHash: UInt160, transaction: Transaction) { - const contract = await this.contracts.tryGet(contractHash); + const contract = await this.native.Management.getContract(this.storage, contractHash); if (contract === undefined) { return { fee: utils.ZERO, size: 0 }; } @@ -547,7 +526,7 @@ export class Blockchain { gas, }, (engine) => { - engine.loadScript({ script: script, initialPosition: offset }); + engine.loadScript({ script, initialPosition: offset }); engine.execute(); return utils.getCallReceipt(engine, container); diff --git a/packages/neo-one-node-blockchain/src/PersistingBlockchain.ts b/packages/neo-one-node-blockchain/src/PersistingBlockchain.ts index b46a031b44..a8c345c51f 100644 --- a/packages/neo-one-node-blockchain/src/PersistingBlockchain.ts +++ b/packages/neo-one-node-blockchain/src/PersistingBlockchain.ts @@ -1,5 +1,5 @@ // tslint:disable no-array-mutation no-object-mutation -import { TriggerType, VMState, common } from '@neo-one/client-common'; +import { common, TriggerType, VMState } from '@neo-one/client-common'; import { ApplicationExecuted, Block, SnapshotHandler, Transaction, VM } from '@neo-one/node-core'; import { PersistNativeContractsError, PostPersistError } from './errors'; import { utils } from './utils'; diff --git a/packages/neo-one-node-blockchain/src/utils.ts b/packages/neo-one-node-blockchain/src/utils.ts index 886f2d9daa..bf06e6a596 100644 --- a/packages/neo-one-node-blockchain/src/utils.ts +++ b/packages/neo-one-node-blockchain/src/utils.ts @@ -50,12 +50,6 @@ const getCallReceipt = (engine: ApplicationEngine, container?: Verifiable) => ({ }); const verifyContract = async (contract: ContractState, vm: VM, transaction: Transaction) => { - const verify = contract.manifest.abi.getMethod('verify'); - if (verify === undefined) { - throw new InvalidFormatError(`the smart contract ${contract.scriptHash} does not have a verify method`); - } - - const init = contract.manifest.abi.getMethod('_initialize'); const gas = vm.withApplicationEngine( { trigger: TriggerType.Verification, @@ -64,21 +58,24 @@ const verifyContract = async (contract: ContractState, vm: VM, transaction: Tran gas: common.TWENTY_FIXED8, }, (engine) => { - engine.loadScript({ - script: contract.script, + const loaded = engine.loadContract({ + hash: contract.hash, flags: CallFlags.None, - initialPosition: init ? init.offset : verify.offset, + method: 'verify', + packParameters: true, }); - engine.loadScript({ script: Buffer.from([]), flags: CallFlags.None }); - const result = engine.execute(); + if (!loaded) { + throw new InvalidFormatError(`contract with hash: ${contract.hash} does not have a verify method.`); + } + const result = engine.execute(); if (result === VMState.FAULT) { - throw new ScriptVerifyError(`contract ${contract.scriptHash} returned FAULT state`); + throw new ScriptVerifyError(`contract ${contract.hash} returned FAULT state`); } if (engine.resultStack.length !== 1 || !engine.resultStack[0].getBoolean()) { - throw new ScriptVerifyError(`contract ${contract.scriptHash} returns false`); + throw new ScriptVerifyError(`contract ${contract.hash} returns false`); } return engine.gasConsumed; diff --git a/packages/neo-one-node-blockchain/src/verify.ts b/packages/neo-one-node-blockchain/src/verify.ts index 84005df5cc..d3ab3b7a74 100644 --- a/packages/neo-one-node-blockchain/src/verify.ts +++ b/packages/neo-one-node-blockchain/src/verify.ts @@ -1,123 +1,27 @@ -import { common, TriggerType, UInt160, VMState } from '@neo-one/client-common'; +import { crypto, TriggerType, UInt160, VMState } from '@neo-one/client-common'; import { - BlockchainStorage, CallFlags, - ContractMethodDescriptor, + ContractState, ExecuteScriptResult, - NativeContainer, - SerializableContainer, - Verifiable, - VM, + maxVerificationGas, + VerifyWitnessesOptions, + VerifyWitnessOptions, } from '@neo-one/node-core'; import { BN } from 'bn.js'; -import { ContractMethodError, ContractStateFetchError, WitnessVerifyError } from './errors'; - -const maxVerificationGas = common.fixed8FromDecimal('0.5'); - -interface ApplicationEngineVerifyOptions { - readonly verification: Buffer; - readonly offset: number; - readonly init?: ContractMethodDescriptor; -} - -const getApplicationEngineVerifyOptions = async ( - verification: Buffer, - storage: BlockchainStorage, - hash: UInt160, - scriptHash: UInt160, -): Promise => { - if (verification.length === 0) { - const contractState = await storage.contracts.tryGet(hash); - if (contractState === undefined) { - throw new ContractStateFetchError(common.uInt160ToHex(hash)); - } - const methodDescriptor = contractState.manifest.abi.getMethod('verify'); - if (methodDescriptor === undefined) { - throw new ContractMethodError('verify', common.uInt160ToHex(hash)); - } - - return { - verification: contractState.script, - offset: methodDescriptor.offset, - init: contractState.manifest.abi.getMethod('_initialize'), - }; - } - - // tslint:disable-next-line: possible-timing-attack TODO: look into this `possible-timing-attack` warning - if (!hash.equals(scriptHash)) { - throw new WitnessVerifyError(); - } - - return { - verification, - offset: 0, - }; -}; - -export const verifyWithApplicationEngine = ( - vm: VM, - verifiable: Verifiable & SerializableContainer, - verification: Buffer, - index: number, - gas: BN, - offset: number, - init?: ContractMethodDescriptor, -): ExecuteScriptResult => - vm.withApplicationEngine( - { trigger: TriggerType.Verification, container: verifiable, snapshot: 'clone', gas }, - (engine) => { - engine.loadScript({ script: verification, flags: CallFlags.None, initialPosition: init ? init.offset : offset }); - engine.loadScript({ script: verifiable.witnesses[index].invocation, flags: CallFlags.None }); - - const state = engine.execute(); - if (state === VMState.FAULT) { - return { result: false, gas }; - } - - const stack = engine.resultStack; - if (stack.length !== 1 || !stack[0].getBoolean()) { - return { result: false, gas }; - } - - return { result: true, gas: gas.sub(engine.gasConsumed) }; - }, - ); - -export const tryVerifyHash = async ( - vm: VM, - hash: UInt160, - index: number, - storage: BlockchainStorage, - verifiable: Verifiable & SerializableContainer, - gas: BN, -): Promise => { - const { verification: verificationScript, scriptHash } = verifiable.witnesses[index]; - try { - const { verification, offset, init } = await getApplicationEngineVerifyOptions( - verificationScript, - storage, - hash, - scriptHash, - ); - - return verifyWithApplicationEngine(vm, verifiable, verification, index, gas, offset, init); - } catch { - return { gas, result: false }; - } -}; -export const verifyWitnesses = async ( - vm: VM, - verifiable: Verifiable & SerializableContainer, - storage: BlockchainStorage, - native: NativeContainer, - gasIn: BN, -): Promise => { +export const verifyWitnesses = async ({ + vm, + verifiable, + storage, + native, + gas: gasIn, + snapshot, +}: VerifyWitnessesOptions): Promise => { if (gasIn.ltn(0)) { return false; } - const gas = gasIn.gt(maxVerificationGas) ? maxVerificationGas : gasIn; + let gas = gasIn.gt(maxVerificationGas) ? maxVerificationGas : gasIn; let hashes: readonly UInt160[]; try { @@ -130,28 +34,97 @@ export const verifyWitnesses = async ( return false; } - const { next, previous } = await hashes - .slice(1) - .reduce; readonly previous: boolean }>>( - async (acc, hash, index) => { - const { next: accNext, previous: accPrevious } = await acc; - const { result, gas: newGas } = await accNext; - if (!result) { - return { - next: Promise.resolve({ result: false, gas: newGas }), - previous: false, - }; + // tslint:disable-next-line: no-loop-statement + for (let i = 0; i < hashes.length; i += 1) { + const { result, gas: gasCost } = await verifyWitness({ + vm, + verifiable, + storage, + native, + hash: hashes[i], + snapshot, + witness: verifiable.witnesses[i], + gas, + }); + + if (!result) { + return false; + } + + gas = gas.sub(gasCost); + } + + return true; +}; + +export const verifyWitness = async ({ + vm, + verifiable, + snapshot: snapshotIn, + storage, + native, + hash, + witness, + gas, +}: VerifyWitnessOptions): Promise => { + const initFee = new BN(0); + const { verification, invocation } = witness; + const callFlags = !crypto.isStandardContract(verification) ? CallFlags.ReadStates : CallFlags.None; + + let contract: ContractState | undefined; + if (verification.length === 0) { + contract = await native.Management.getContract(storage, hash); + if (contract === undefined) { + return { result: false, gas: initFee }; + } + } + + const snapshot = snapshotIn ?? 'clone'; + vm.withSnapshots(({ main }) => { + if (snapshotIn === 'clone') { + main.clone(); + } + }); + + return vm.withApplicationEngine( + { + trigger: TriggerType.Verification, + container: verifiable, + snapshot, + gas, + }, + async (engine) => { + if (contract !== undefined) { + const loadContractResult = engine.loadContract({ + hash, + method: 'verify', + flags: callFlags, + packParameters: true, + }); + if (!loadContractResult) { + return { result: false, gas: initFee }; + } + } else { + // tslint:disable-next-line: possible-timing-attack + if (native.isNative(hash) || hash !== witness.scriptHash) { + return { result: false, gas: initFee }; } - return { - next: tryVerifyHash(vm, hash, index, storage, verifiable, newGas), - previous: accPrevious && result, - }; - }, - Promise.resolve({ next: tryVerifyHash(vm, hashes[0], 0, storage, verifiable, gas), previous: true }), - ); + engine.loadScript({ script: verification, flags: callFlags, scriptHash: hash, initialPosition: 0 }); + } - const { result: finalResult } = await next; + engine.loadScript({ script: invocation, flags: CallFlags.None }); + const result = engine.execute(); - return finalResult && previous; + if (result === VMState.FAULT) { + return { result: false, gas: initFee }; + } + + if (engine.resultStack.length !== 1 || !engine.resultStack[0].getBoolean()) { + return { result: false, gas: initFee }; + } + + return { result: true, gas: engine.gasConsumed }; + }, + ); }; diff --git a/packages/neo-one-node-core/src/ContractState.ts b/packages/neo-one-node-core/src/ContractState.ts index d01606648e..00b4b868c8 100644 --- a/packages/neo-one-node-core/src/ContractState.ts +++ b/packages/neo-one-node-core/src/ContractState.ts @@ -1,19 +1,13 @@ -import { - common, - ContractJSON, - createSerializeWire, - crypto, - IOHelper, - JSONHelper, - UInt160, -} from '@neo-one/client-common'; +import { common, ContractJSON, IOHelper, JSONHelper, UInt160 } from '@neo-one/client-common'; import { ContractStateModel } from '@neo-one/client-full-common'; import { ContractManifest } from './manifest'; -import { DeserializeWireBaseOptions, DeserializeWireOptions } from './Serializable'; -import { BinaryReader, utils } from './utils'; +import { utils } from './utils'; +import { StackItem, assertArrayStackItem } from './StackItems'; export interface ContractStateAdd { readonly id: number; + readonly updateCounter: number; + readonly hash: UInt160; readonly script: Buffer; readonly manifest: ContractManifest; } @@ -21,37 +15,27 @@ export interface ContractStateAdd { export type ContractKey = UInt160; export class ContractState extends ContractStateModel { - public static deserializeWireBase(options: DeserializeWireBaseOptions): ContractState { - const { reader } = options; - const id = reader.readInt32LE(); - const script = reader.readVarBytesLE(); - const manifest = ContractManifest.deserializeWireBase(options); + public static fromStackItem(stackItem: StackItem): ContractState { + const { array } = assertArrayStackItem(stackItem); + const id = array[0].getInteger().toNumber(); + const updateCounter = array[1].getInteger().toNumber(); + const hash = common.bufferToUInt160(array[2].getBuffer()); + const script = array[3].getBuffer(); + const manifest = ContractManifest.parseBytes(array[4].getBuffer()); - return new this({ + return new ContractState({ id, + updateCounter, + hash, script, manifest, }); } - public static deserializeWire(options: DeserializeWireOptions): ContractState { - return this.deserializeWireBase({ - context: options.context, - reader: new BinaryReader(options.buffer), - }); - } - - public readonly serializeWire = createSerializeWire(this.serializeWireBase.bind(this)); - - private readonly scriptHashInternal = utils.lazy(() => common.asUInt160(crypto.hash160(this.script))); private readonly sizeInternal = utils.lazy( () => IOHelper.sizeOfUInt32LE + IOHelper.sizeOfVarBytesLE(this.script) + this.manifest.size, ); - public get scriptHash() { - return this.scriptHashInternal(); - } - public get size() { return this.sizeInternal(); } @@ -59,6 +43,8 @@ export class ContractState extends ContractStateModel { public clone() { return new ContractState({ id: this.id, + updateCounter: this.updateCounter, + hash: this.hash, script: this.script, manifest: this.manifest, }); @@ -67,7 +53,8 @@ export class ContractState extends ContractStateModel { public serializeJSON(): ContractJSON { return { id: this.id, - hash: JSONHelper.writeUInt160(this.scriptHash), + updatecounter: this.updateCounter, + hash: JSONHelper.writeUInt160(this.hash), script: JSONHelper.writeBase64Buffer(this.script), manifest: this.manifest.serializeJSON(), }; diff --git a/packages/neo-one-node-core/src/DesignationRole.ts b/packages/neo-one-node-core/src/DesignationRole.ts new file mode 100644 index 0000000000..720e58f1a9 --- /dev/null +++ b/packages/neo-one-node-core/src/DesignationRole.ts @@ -0,0 +1,4 @@ +export enum DesignationRole { + StateValidator = 4, + Oracle = 8, +} diff --git a/packages/neo-one-node-core/src/Native.ts b/packages/neo-one-node-core/src/Native.ts index c63820a9bd..ed2e92fab7 100644 --- a/packages/neo-one-node-core/src/Native.ts +++ b/packages/neo-one-node-core/src/Native.ts @@ -1,9 +1,14 @@ import { ECPoint, UInt160 } from '@neo-one/client-common'; import { BN } from 'bn.js'; +import { ContractState } from './ContractState'; +import { DesignationRole } from './DesignationRole'; +import { OracleRequest } from './OracleRequest'; import { ReadFindStorage } from './Storage'; import { StorageItem } from './StorageItem'; import { StorageKey } from './StorageKey'; +export type OracleRequestResults = ReadonlyArray; + export interface NativeContractStorageContext { readonly storages: ReadFindStorage; } @@ -13,7 +18,7 @@ export interface NativeContract { readonly name: string; } -export interface NEP5NativeContract extends NativeContract { +export interface NEP17NativeContract extends NativeContract { readonly symbol: string; readonly decimals: number; @@ -21,14 +26,16 @@ export interface NEP5NativeContract extends NativeContract { readonly balanceOf: (storage: NativeContractStorageContext, account: UInt160) => Promise; } -export interface GASContract extends NEP5NativeContract {} +export interface GASContract extends NEP17NativeContract {} export interface PolicyContract extends NativeContract { readonly getMaxTransactionsPerBlock: (storage: NativeContractStorageContext) => Promise; readonly getMaxBlockSize: (storage: NativeContractStorageContext) => Promise; readonly getMaxBlockSystemFee: (storage: NativeContractStorageContext) => Promise; readonly getFeePerByte: (storage: NativeContractStorageContext) => Promise; - readonly getBlockedAccounts: (storage: NativeContractStorageContext) => Promise; + readonly getExecFeeFactor: (storage: NativeContractStorageContext) => Promise; + readonly getStoragePrice: (storage: NativeContractStorageContext) => Promise; + readonly isBlocked: (storage: NativeContractStorageContext, account: UInt160) => Promise; } export interface Candidate { @@ -36,21 +43,45 @@ export interface Candidate { readonly votes: BN; } -export interface NEOContract extends NEP5NativeContract { +export interface NEOContract extends NEP17NativeContract { readonly totalAmount: BN; readonly effectiveVoterTurnout: number; readonly totalSupply: () => Promise; readonly getCandidates: (storage: NativeContractStorageContext) => Promise; - readonly getValidators: (storage: NativeContractStorageContext) => Promise; readonly getCommittee: (storage: NativeContractStorageContext) => Promise; readonly getCommitteeAddress: (storage: NativeContractStorageContext) => Promise; readonly unclaimedGas: (storage: NativeContractStorageContext, account: UInt160, end: number) => Promise; readonly getNextBlockValidators: (storage: NativeContractStorageContext) => Promise; + readonly computeNextBlockValidators: (storage: NativeContractStorageContext) => Promise; +} + +export interface ManagementContract extends NativeContract { + readonly getContract: (storage: NativeContractStorageContext, hash: UInt160) => Promise; + readonly listContracts: (storage: NativeContractStorageContext) => Promise; +} + +export interface DesignationContract extends NativeContract { + readonly getDesignatedByRole: ( + storage: NativeContractStorageContext, + role: DesignationRole, + height: number, + index: number, + ) => Promise; +} + +export interface OracleContract extends NativeContract { + readonly getRequest: (storage: NativeContractStorageContext, id: BN) => Promise; + readonly getRequests: (storage: NativeContractStorageContext) => Promise; + readonly getRequestsByUrl: (storage: NativeContractStorageContext, url: string) => Promise; } export interface NativeContainer { readonly GAS: GASContract; readonly NEO: NEOContract; readonly Policy: PolicyContract; + readonly Management: ManagementContract; + readonly Designation: DesignationContract; + readonly Oracle: OracleContract; + readonly isNative: (hash: UInt160) => boolean; } diff --git a/packages/neo-one-node-core/src/OracleRequest.ts b/packages/neo-one-node-core/src/OracleRequest.ts new file mode 100644 index 0000000000..77e0dfd034 --- /dev/null +++ b/packages/neo-one-node-core/src/OracleRequest.ts @@ -0,0 +1,62 @@ +import { UInt256, UInt160, common } from '@neo-one/client-common'; +import { BN } from 'bn.js'; +import { StackItem, assertArrayStackItem } from './StackItems'; + +interface OracleRequestAdd { + readonly originalTxid: UInt256; + readonly gasForResponse: BN; + readonly url: string; + readonly filter: string; + readonly callbackContract: UInt160; + readonly callbackMethod: string; + readonly userData: Buffer; +} + +export class OracleRequest { + public static fromStackItem(stackItem: StackItem): OracleRequest { + const { array } = assertArrayStackItem(stackItem); + const originalTxid = common.bufferToUInt256(array[0].getBuffer()); + const gasForResponse = array[1].getInteger(); + const url = array[2].getString(); + const filter = array[3].getString(); + const callbackContract = common.bufferToUInt160(array[4].getBuffer()); + const callbackMethod = array[5].getString(); + const userData = array[6].getBuffer(); + + return new OracleRequest({ + originalTxid, + gasForResponse, + url, + filter, + callbackContract, + callbackMethod, + userData, + }); + } + + public originalTxid: UInt256; + public gasForResponse: BN; + public url: string; + public filter: string; + public callbackContract: UInt160; + public callbackMethod: string; + public userData: Buffer; + + public constructor({ + originalTxid, + gasForResponse, + url, + filter, + callbackContract, + callbackMethod, + userData, + }: OracleRequestAdd) { + this.originalTxid = originalTxid; + this.gasForResponse = gasForResponse; + this.url = url; + this.filter = filter; + this.callbackContract = callbackContract; + this.callbackMethod = callbackMethod; + this.userData = userData; + } +} diff --git a/packages/neo-one-node-core/src/Storage.ts b/packages/neo-one-node-core/src/Storage.ts index 2cefae8ca9..8f55036282 100644 --- a/packages/neo-one-node-core/src/Storage.ts +++ b/packages/neo-one-node-core/src/Storage.ts @@ -134,12 +134,10 @@ export interface BlockchainStorage { readonly applicationLogs: ReadStorage; // readonly consensusState: ReadMetadataStorage; readonly transactions: ReadStorage; - readonly contracts: ReadStorage; readonly storages: ReadFindStorage; readonly headerHashList: ReadStorage; readonly blockHashIndex: ReadMetadataStorage; readonly headerHashIndex: ReadMetadataStorage; - readonly contractID: ReadMetadataStorage; } export interface Storage extends BlockchainStorage { diff --git a/packages/neo-one-node-core/src/TransactionVerificationContext.ts b/packages/neo-one-node-core/src/TransactionVerificationContext.ts index b913ec208f..1cdbc9fc1a 100644 --- a/packages/neo-one-node-core/src/TransactionVerificationContext.ts +++ b/packages/neo-one-node-core/src/TransactionVerificationContext.ts @@ -1,7 +1,7 @@ -import { common, UInt160, UInt160Hex } from '@neo-one/client-common'; +import { common, UInt160, UInt160Hex, UInt256Hex } from '@neo-one/client-common'; import { BN } from 'bn.js'; import { NativeContractStorageContext } from './Native'; -import { Transaction } from './transaction'; +import { isOracleResponse, Transaction } from './transaction'; const assertSender = (sender: UInt160 | undefined) => { if (sender === undefined) { @@ -19,13 +19,20 @@ export interface TransactionVerificationContextAdd { export class TransactionVerificationContext { private readonly getGasBalance: (storage: NativeContractStorageContext, sender: UInt160) => Promise; private readonly mutableSenderFee: Record; + private readonly mutableOracleResponses: Record; public constructor({ getGasBalance }: TransactionVerificationContextAdd) { this.getGasBalance = getGasBalance; this.mutableSenderFee = {}; + this.mutableOracleResponses = {}; } public addTransaction(tx: Transaction) { + const oracle = tx.getAttribute(isOracleResponse); + if (oracle !== undefined) { + this.mutableOracleResponses[oracle.id.toString()] = tx.hashHex; + } + const key = common.uInt160ToHex(assertSender(tx.sender)); const maybeFee = this.mutableSenderFee[key] ?? new BN(0); this.mutableSenderFee[key] = maybeFee.add(tx.systemFee).add(tx.networkFee); @@ -37,7 +44,16 @@ export class TransactionVerificationContext { const maybeFee = this.mutableSenderFee[common.uInt160ToHex(sender)] ?? new BN(0); const totalFee = maybeFee.add(tx.systemFee).add(tx.networkFee); - return balance.gte(totalFee); + if (balance.lt(totalFee)) { + return false; + } + + const oracle = tx.getAttribute(isOracleResponse); + if (oracle !== undefined && this.mutableOracleResponses[oracle.id.toString()] !== undefined) { + return false; + } + + return true; } public removeTransaction(tx: Transaction) { @@ -56,5 +72,11 @@ export class TransactionVerificationContext { } else { this.mutableSenderFee[key] = newFee; } + + const oracle = tx.getAttribute(isOracleResponse); + if (oracle !== undefined) { + // tslint:disable-next-line: no-dynamic-delete + delete this.mutableOracleResponses[oracle.id.toString()]; + } } } diff --git a/packages/neo-one-node-core/src/Verifiable.ts b/packages/neo-one-node-core/src/Verifiable.ts index cf06cbba51..a4e49a50ee 100644 --- a/packages/neo-one-node-core/src/Verifiable.ts +++ b/packages/neo-one-node-core/src/Verifiable.ts @@ -1,11 +1,12 @@ -import { UInt160 } from '@neo-one/client-common'; +import { common, UInt160 } from '@neo-one/client-common'; import { BN } from 'bn.js'; import { NativeContainer } from './Native'; import { SerializableContainer } from './Serializable'; import { BlockchainStorage } from './Storage'; -import { VM } from './vm'; +import { SnapshotName, VM } from './vm'; import { Witness } from './Witness'; +export const maxVerificationGas = common.fixed8FromDecimal('0.5'); export interface Verifiable { readonly getScriptHashesForVerifying: (context: { readonly storage: BlockchainStorage; @@ -19,19 +20,31 @@ export interface ExecuteScriptResult { readonly result: boolean; } -export type VerifyWitnesses = ( - vm: VM, - verifiable: SerializableContainer, - storage: BlockchainStorage, - native: NativeContainer, - gasIn: BN, -) => Promise; +export interface VerifyWitnessesOptions { + readonly vm: VM; + readonly verifiable: SerializableContainer; + readonly storage: BlockchainStorage; + readonly native: NativeContainer; + readonly gas: BN; + readonly snapshot?: SnapshotName; +} + +export type VerifyWitnesses = (options: VerifyWitnessesOptions) => Promise; + +export interface VerifyWitnessOptions extends VerifyWitnessesOptions { + readonly hash: UInt160; + readonly witness: Witness; +} + +export type VerifyWitness = (options: VerifyWitnessOptions) => Promise; export interface VerifyOptions { + readonly height: number; readonly vm: VM; readonly storage: BlockchainStorage; readonly native: NativeContainer; readonly verifyWitnesses: VerifyWitnesses; + readonly verifyWitness: VerifyWitness; } /* I think all of this might be a useless abstraction of blockchain properties. */ diff --git a/packages/neo-one-node-core/src/errors.ts b/packages/neo-one-node-core/src/errors.ts index 621129b3ac..a86fdb8107 100644 --- a/packages/neo-one-node-core/src/errors.ts +++ b/packages/neo-one-node-core/src/errors.ts @@ -1,3 +1,4 @@ +import { OracleResponseCode } from '@neo-one/client-common'; import { makeErrorWithCode } from '@neo-one/utils'; // tslint:disable-next-line export-name @@ -74,3 +75,8 @@ export const InvalidOpCodeError = makeErrorWithCode( 'INVALID_OP_CODE_ERROR', (value: number) => `Cannot find fee for OpCode ${value}.`, ); +export const InvalidOracleResultError = makeErrorWithCode( + 'INVALID_ORACLE_RESULT_ERROR', + (code: OracleResponseCode, resultLength: number) => + `Expected result.length to be 0 with response code ${OracleResponseCode[code]}, found: ${resultLength}`, +); diff --git a/packages/neo-one-node-core/src/index.ts b/packages/neo-one-node-core/src/index.ts index d14f6afea3..040a19d6ec 100644 --- a/packages/neo-one-node-core/src/index.ts +++ b/packages/neo-one-node-core/src/index.ts @@ -16,7 +16,9 @@ export * from './HeaderHashList'; export * from './Native'; export * from './network'; export * from './Notification'; +export * from './OracleRequest'; export * from './payload'; +export * from './DesignationRole'; export * from './Serializable'; export * from './Settings'; export * from './Signer'; diff --git a/packages/neo-one-node-core/src/manifest/ContractManifest.ts b/packages/neo-one-node-core/src/manifest/ContractManifest.ts index d9159f8e3f..474a90b3c7 100644 --- a/packages/neo-one-node-core/src/manifest/ContractManifest.ts +++ b/packages/neo-one-node-core/src/manifest/ContractManifest.ts @@ -1,24 +1,14 @@ import { ContractManifestJSON, IOHelper, JSONHelper, UInt160 } from '@neo-one/client-common'; -import { ContractManifestModel, getContractProperties } from '@neo-one/client-full-common'; -import { DeserializeWireBaseOptions, DeserializeWireOptions } from '../Serializable'; +import { ContractManifestModel } from '@neo-one/client-full-common'; import { BinaryReader, utils } from '../utils'; import { ContractABI } from './ContractABI'; import { ContractGroup } from './ContractGroup'; import { ContractPermission } from './ContractPermission'; export class ContractManifest extends ContractManifestModel { - public static deserializeWireBase(options: DeserializeWireBaseOptions): ContractManifest { - const { reader } = options; - const json = JSON.parse(reader.readVarString(this.maxLength)); - - return this.deserializeJSON(json); - } - - public static deserializeWire(options: DeserializeWireOptions): ContractManifest { - return this.deserializeWireBase({ - context: options.context, - reader: new BinaryReader(options.buffer), - }); + public static parseBytes(bytes: Buffer) { + const reader = new BinaryReader(bytes); + return this.deserializeJSON(JSON.parse(reader.readVarString(this.maxLength))); } private static deserializeJSON(json: ContractManifestJSON) { diff --git a/packages/neo-one-node-core/src/payload/ConsensusPayload.ts b/packages/neo-one-node-core/src/payload/ConsensusPayload.ts index 9cf52cce44..2ed061ac3b 100644 --- a/packages/neo-one-node-core/src/payload/ConsensusPayload.ts +++ b/packages/neo-one-node-core/src/payload/ConsensusPayload.ts @@ -1,6 +1,7 @@ import { AccountContract, BinaryWriter, + common, createSerializeWire, crypto, ECPoint, @@ -9,16 +10,14 @@ import { PrivateKey, } from '@neo-one/client-common'; import { ContractParametersContext } from '../ContractParametersContext'; -import { NativeContainer } from '../Native'; import { DeserializeWireBaseOptions, DeserializeWireOptions, SerializableContainer, SerializableContainerType, } from '../Serializable'; -import { BlockchainStorage } from '../Storage'; -import { BinaryReader, utils } from '../utils'; -import { Verifiable, VerifyOptions } from '../Verifiable'; +import { BinaryReader } from '../utils'; +import { VerifyOptions } from '../Verifiable'; import { Witness } from '../Witness'; import { ConsensusMessage } from './message'; import { UnsignedConsensusPayload, UnsignedConsensusPayloadAdd } from './UnsignedConsensusPayload'; @@ -26,12 +25,7 @@ import { UnsignedConsensusPayload, UnsignedConsensusPayloadAdd } from './Unsigne export interface ConsensusPayloadAdd extends UnsignedConsensusPayloadAdd { readonly witness: Witness; } - -export interface VerifyConsensusPayloadOptions extends VerifyOptions { - readonly height: number; -} - -export class ConsensusPayload extends UnsignedConsensusPayload { +export class ConsensusPayload extends UnsignedConsensusPayload implements SerializableContainer { public static sign( payload: UnsignedConsensusPayload, privateKey: PrivateKey, @@ -117,11 +111,11 @@ export class ConsensusPayload extends UnsignedConsensusPayload { return this.consensusMessage as T; } - public async verify(options: VerifyConsensusPayloadOptions) { + public async verify(options: VerifyOptions) { if (this.blockIndex <= options.height) { return false; } - return options.verifyWitnesses(options.vm, this, options.storage, options.native, 0.02); + return options.verifyWitnesses(options.vm, this, options.storage, options.native, common.fixed8FromDecimal('0.02')); } } diff --git a/packages/neo-one-node-core/src/transaction/Transaction.ts b/packages/neo-one-node-core/src/transaction/Transaction.ts index cb46f4499e..9b3d4916c5 100644 --- a/packages/neo-one-node-core/src/transaction/Transaction.ts +++ b/packages/neo-one-node-core/src/transaction/Transaction.ts @@ -1,12 +1,15 @@ import { AttributeTypeModel, common, + crypto, InvalidFormatError, IOHelper, JSONHelper, MAX_TRANSACTION_SIZE, MAX_VALID_UNTIL_BLOCK_INCREMENT, + multiSignatureContractCost, scriptHashToAddress, + signatureContractCost, TransactionJSON, TransactionModel, TransactionModelAdd, @@ -26,7 +29,7 @@ import { import { Signer } from '../Signer'; import { TransactionVerificationContext } from '../TransactionVerificationContext'; import { BinaryReader, utils } from '../utils'; -import { Verifiable, VerifyOptions } from '../Verifiable'; +import { maxVerificationGas, Verifiable, VerifyOptions } from '../Verifiable'; import { Witness } from '../Witness'; import { Attribute, deserializeAttribute } from './attributes'; @@ -174,20 +177,19 @@ export class Transaction IOHelper.sizeOfArray(this.witnesses, (witness) => witness.size), ); - public async verifyForEachBlock( + public async verifyStateDependent( verifyOptions: VerifyOptions, transactionContext?: TransactionVerificationContext, ): Promise { - const { storage, native } = verifyOptions; + const { storage, native, verifyWitness, vm } = verifyOptions; const { index } = await storage.blockHashIndex.get(); - if (this.validUntilBlock < index || this.validUntilBlock > index + MAX_VALID_UNTIL_BLOCK_INCREMENT) { + if (this.validUntilBlock <= index || this.validUntilBlock > index + MAX_VALID_UNTIL_BLOCK_INCREMENT) { return VerifyResultModel.Expired; } const hashes = this.getScriptHashesForVerifying(); - const setHashes = new Set(hashes); - const blockedAccounts = await native.Policy.getBlockedAccounts(storage); - if (blockedAccounts.some((account) => setHashes.has(account))) { + const blocked = await Promise.all(hashes.map(async (hash) => native.Policy.isBlocked(storage, hash))); + if (blocked.some((bool) => bool)) { return VerifyResultModel.PolicyFail; } @@ -212,21 +214,64 @@ export class Transaction return VerifyResultModel.Invalid; } - const verifyHashes = await Promise.all( - hashes.map(async (hash, idx) => { - if (this.witnesses[idx].verification.length > 0) { - return true; + const [feePerByte, execFeeFactor] = await Promise.all([ + native.Policy.getFeePerByte(storage), + native.Policy.getExecFeeFactor(storage), + ]); + + let netFee = this.networkFee.sub(feePerByte.muln(this.size)); + // tslint:disable-next-line: no-loop-statement + for (let i = 0; i < hashes.length; i += 1) { + const witness = this.witnesses[i]; + const multiSigResult = crypto.isMultiSigContractWithResult(witness.verification); + if (multiSigResult.result) { + const { m, n } = multiSigResult; + netFee = netFee.sub(new BN(multiSignatureContractCost(m, n).toString(), 10).muln(execFeeFactor)); + } else if (crypto.isSignatureContract(witness.verification)) { + netFee = netFee.sub(new BN(signatureContractCost.toString(), 10).muln(execFeeFactor)); + } else { + const { result, gas } = await verifyWitness(vm, this, storage, native, hashes[i], witness, netFee); + if (!result) { + return VerifyResultModel.InsufficientFunds; } - const state = await storage.contracts.tryGet(hash); + netFee = netFee.sub(gas); + } + if (netFee.ltn(0)) { + return VerifyResultModel.InsufficientFunds; + } + } - return state !== undefined; - }), - ); + return VerifyResultModel.Succeed; + } - if (verifyHashes.some((value) => !value)) { + public async verifyStateIndependent(verifyOptions: VerifyOptions) { + const { storage, native, verifyWitness, vm } = verifyOptions; + if (this.size > MAX_TRANSACTION_SIZE) { + return VerifyResultModel.Invalid; + } + const hashes = this.getScriptHashesForVerifying(); + if (hashes.length !== this.witnesses.length) { return VerifyResultModel.Invalid; } + // tslint:disable-next-line: no-loop-statement + for (let i = 0; i < hashes.length; i += 1) { + if (crypto.isStandardContract(this.witnesses[i].verification)) { + const { result } = await verifyWitness( + vm, + this, + storage, + native, + hashes[i], + this.witnesses[i], + maxVerificationGas, + ); + if (!result) { + return VerifyResultModel.Invalid; + } + } + } + return VerifyResultModel.Succeed; } @@ -234,26 +279,12 @@ export class Transaction verifyOptions: VerifyOptions, verifyContext?: TransactionVerificationContext, ): Promise { - const { native, storage, verifyWitnesses, vm } = verifyOptions; - const result = await this.verifyForEachBlock(verifyOptions, verifyContext); - if (result !== VerifyResultModel.Succeed) { - return result; - } - if (this.size > MAX_TRANSACTION_SIZE) { - return VerifyResultModel.Invalid; - } - const feePerByte = await native.Policy.getFeePerByte(storage); - const netFee = this.networkFee.sub(feePerByte.muln(this.size)); - if (netFee.ltn(0)) { - return VerifyResultModel.InsufficientFunds; + const independentResult = await this.verifyStateIndependent(verifyOptions); + if (independentResult !== VerifyResultModel.Succeed) { + return independentResult; } - const witnessVerify = await verifyWitnesses(vm, this, storage, native, netFee); - if (!witnessVerify) { - return VerifyResultModel.Invalid; - } - - return VerifyResultModel.Succeed; + return this.verifyStateDependent(verifyOptions, verifyContext); } public serializeJSON(): TransactionJSON { diff --git a/packages/neo-one-node-core/src/transaction/attributes/Attribute.ts b/packages/neo-one-node-core/src/transaction/attributes/Attribute.ts index 2b041f0ea3..dc06e15031 100644 --- a/packages/neo-one-node-core/src/transaction/attributes/Attribute.ts +++ b/packages/neo-one-node-core/src/transaction/attributes/Attribute.ts @@ -1,8 +1,9 @@ import { assertAttributeType, AttributeTypeModel, InvalidFormatError } from '@neo-one/client-common'; import { DeserializeWireBaseOptions } from '../../Serializable'; import { HighPriorityAttribute } from './HighPriorityAttribute'; +import { OracleResponse } from './OracleResponse'; -export type Attribute = HighPriorityAttribute; +export type Attribute = HighPriorityAttribute | OracleResponse; export const deserializeAttribute = (options: DeserializeWireBaseOptions): Attribute => { const { reader } = options; @@ -11,8 +12,16 @@ export const deserializeAttribute = (options: DeserializeWireBaseOptions): Attri switch (type) { case AttributeTypeModel.HighPriority: - return new HighPriorityAttribute(); + return HighPriorityAttribute.deserializeWithoutType(reader); + case AttributeTypeModel.OracleResponse: + return OracleResponse.deserializeWithoutType(reader); default: throw new InvalidFormatError(`Attribute type ${type} not yet implemented`); } }; + +export const getIsAttribute = (type: AttributeTypeModel) => (attr: Attribute): attr is T => + attr.type === type; + +export const isHighPriorityAttribute = getIsAttribute(AttributeTypeModel.HighPriority); +export const isOracleResponse = getIsAttribute(AttributeTypeModel.OracleResponse); diff --git a/packages/neo-one-node-core/src/transaction/attributes/AttributeBase.ts b/packages/neo-one-node-core/src/transaction/attributes/AttributeBase.ts index cc5ccddb5d..9816f814ee 100644 --- a/packages/neo-one-node-core/src/transaction/attributes/AttributeBase.ts +++ b/packages/neo-one-node-core/src/transaction/attributes/AttributeBase.ts @@ -1,34 +1,7 @@ -import { - AttributeBaseModel, - AttributeJSON, - AttributeTypeModel, - InvalidFormatError, - IOHelper, - toJSONAttributeType, -} from '@neo-one/client-common'; -import { DeserializeWireBaseOptions, SerializableJSON } from '../../Serializable'; +import { AttributeJSON, SerializableJSON } from '@neo-one/client-common'; import { VerifyOptions } from '../../Verifiable'; import { Transaction } from '../Transaction'; -export const createDeserializeAttributeType = (type: AttributeTypeModel) => (options: DeserializeWireBaseOptions) => { - const { reader } = options; - const byte = reader.readUInt8(); - if (byte !== type) { - throw new InvalidFormatError(`Expected attribute type: ${type}, found: ${byte}`); - } - - return type; -}; - -export abstract class AttributeBase extends AttributeBaseModel implements SerializableJSON { - public serializeJSON(): AttributeJSON { - return { - type: toJSONAttributeType(this.type), - }; - } - - // Must not be implemented in C# land yet? - public async verify(_verifyOptions: VerifyOptions, _tx: Transaction) { - return Promise.resolve(true); - } +export interface AttributeBase extends SerializableJSON { + readonly verify: (verifyOptions: VerifyOptions, tx: Transaction) => Promise; } diff --git a/packages/neo-one-node-core/src/transaction/attributes/HighPriorityAttribute.ts b/packages/neo-one-node-core/src/transaction/attributes/HighPriorityAttribute.ts index 42bc09f28f..e1cd54a250 100644 --- a/packages/neo-one-node-core/src/transaction/attributes/HighPriorityAttribute.ts +++ b/packages/neo-one-node-core/src/transaction/attributes/HighPriorityAttribute.ts @@ -1,7 +1,23 @@ -import { AttributeTypeModel } from '@neo-one/client-common'; +import { HighPriorityAttributeJSON, HighPriorityAttributeModel } from '@neo-one/client-common'; +import { BinaryReader } from '../../utils'; +import { VerifyOptions } from '../../Verifiable'; +import { Transaction } from '../Transaction'; import { AttributeBase } from './AttributeBase'; -export class HighPriorityAttribute extends AttributeBase { - public readonly type = AttributeTypeModel.HighPriority; - public readonly allowMultiple = false; +export class HighPriorityAttribute + extends HighPriorityAttributeModel + implements AttributeBase { + public static deserializeWithoutType(_reader: BinaryReader) { + return new HighPriorityAttribute(); + } + + public serializeJSON(): HighPriorityAttributeJSON { + return { + type: 'HighPriority', + }; + } + + public async verify(_verifyOptions: VerifyOptions, _tx: Transaction) { + return Promise.resolve(true); + } } diff --git a/packages/neo-one-node-core/src/transaction/attributes/OracleResponse.ts b/packages/neo-one-node-core/src/transaction/attributes/OracleResponse.ts new file mode 100644 index 0000000000..7d4bb36115 --- /dev/null +++ b/packages/neo-one-node-core/src/transaction/attributes/OracleResponse.ts @@ -0,0 +1,84 @@ +import { + assertOracleResponseCode, + common, + crypto, + JSONHelper, + OracleResponseCode, + OracleResponseJSON, + OracleResponseModel, + ScriptBuilder, + utils, + WitnessScopeModel, +} from '@neo-one/client-common'; +import { DesignationRole } from '../../DesignationRole'; +import { InvalidOracleResultError } from '../../errors'; +import { BinaryReader } from '../../utils'; +import { VerifyOptions } from '../../Verifiable'; +import { Transaction } from '../Transaction'; +import { AttributeBase } from './AttributeBase'; + +const maxResultSize = 65535; + +const getFixedScript = utils.lazy(() => { + const builder = new ScriptBuilder(); + builder.emitAppCall(common.nativeHashes.Oracle, 'finish'); + + return builder.build(); +}); + +export class OracleResponse extends OracleResponseModel implements AttributeBase { + public static readonly fixedScript = getFixedScript(); + public static deserializeWithoutType(reader: BinaryReader): OracleResponse { + const id = reader.readUInt64LE(); + const code = assertOracleResponseCode(reader.readUInt8()); + const result = reader.readVarBytesLE(maxResultSize); + + if (code !== OracleResponseCode.Success && result.length > 0) { + throw new InvalidOracleResultError(code, result.length); + } + + return new OracleResponse({ + id, + code, + result, + }); + } + + public serializeJSON(): OracleResponseJSON { + return { + type: 'OracleResponse', + id: this.id.toString(), + code: this.code, + result: JSONHelper.writeBase64Buffer(this.result), + }; + } + + public async verify({ native, storage, height }: VerifyOptions, tx: Transaction) { + if (tx.signers.some((signer) => signer.scopes !== WitnessScopeModel.None)) { + return false; + } + + if (!tx.script.equals(OracleResponse.fixedScript)) { + return false; + } + + const request = await native.Oracle.getRequest(storage, this.id); + if (request === undefined) { + return false; + } + + if (!tx.networkFee.add(tx.systemFee).eq(request.gasForResponse)) { + return false; + } + + const designated = await native.Designation.getDesignatedByRole( + storage, + DesignationRole.Oracle, + height, + height + 1, + ); + const oracleAccount = crypto.getConsensusAddress(designated); + + return tx.signers.some((signer) => signer.account.equals(oracleAccount)); + } +} diff --git a/packages/neo-one-node-core/src/vm.ts b/packages/neo-one-node-core/src/vm.ts index 01755a30a8..853f04d171 100644 --- a/packages/neo-one-node-core/src/vm.ts +++ b/packages/neo-one-node-core/src/vm.ts @@ -1,4 +1,4 @@ -import { TriggerType, UInt256, VMState, Log, UInt160 } from '@neo-one/client-common'; +import { Log, TriggerType, UInt160, UInt256, VMState } from '@neo-one/client-common'; import { BN } from 'bn.js'; import { Block } from './Block'; import { CallFlags } from './CallFlags'; @@ -47,6 +47,13 @@ export interface LoadScriptOptions { readonly initialPosition?: number; } +export interface LoadContractOptions { + readonly hash: UInt160; + readonly flags: CallFlags; + readonly method: string; + readonly packParameters?: boolean; +} + export interface ApplicationEngine { readonly trigger: TriggerType; readonly gasConsumed: BN; @@ -55,6 +62,7 @@ export interface ApplicationEngine { readonly notifications: readonly StackItem[]; readonly logs: readonly VMLog[]; readonly loadScript: (options: LoadScriptOptions) => boolean; + readonly loadContract: (options: LoadContractOptions) => boolean; readonly execute: () => VMState; } @@ -71,6 +79,7 @@ export interface SnapshotHandler { readonly setPersistingBlock: (block: Block) => boolean; readonly hasPersistingBlock: () => boolean; // TODO: type the returning changeSet + // tslint:disable-next-line: no-any readonly getChangeSet: () => any; readonly clone: () => void; } @@ -95,6 +104,7 @@ export interface VM { readonly withSnapshots: ( func: (snapshots: { readonly main: SnapshotHandler; readonly clone: Omit }) => T, ) => T; - readonly updateStore: (storage: ReadonlyArray<{ key: Buffer; value: Buffer }>) => void; + readonly updateStore: (storage: ReadonlyArray<{ readonly key: Buffer; readonly value: Buffer }>) => void; + // tslint:disable-next-line: no-any readonly test: () => any; } diff --git a/packages/neo-one-node-native/src/CachedCommittee.ts b/packages/neo-one-node-native/src/CachedCommittee.ts new file mode 100644 index 0000000000..7aaa73d78a --- /dev/null +++ b/packages/neo-one-node-native/src/CachedCommittee.ts @@ -0,0 +1,31 @@ +import { common, ECPoint } from '@neo-one/client-common'; +import { assertArrayStackItem, assertStructStackItem, StackItem } from '@neo-one/node-core'; +import { BN } from 'bn.js'; + +interface CachedCommitteeElement { + readonly publicKey: ECPoint; + readonly votes: BN; +} + +export class CachedCommittee { + public static fromStackItem(item: StackItem) { + const arrayItem = assertArrayStackItem(item); + + const members = arrayItem.array.map((element) => { + const structItem = assertStructStackItem(element); + + return { + publicKey: common.bufferToECPoint(structItem.array[0].getBuffer()), + votes: structItem.array[1].getInteger(), + }; + }); + + return new CachedCommittee(members); + } + + public readonly members: readonly CachedCommitteeElement[]; + + public constructor(members: readonly CachedCommitteeElement[]) { + this.members = members; + } +} diff --git a/packages/neo-one-node-native/src/DesignationContract.ts b/packages/neo-one-node-native/src/DesignationContract.ts new file mode 100644 index 0000000000..7d4c9add5b --- /dev/null +++ b/packages/neo-one-node-native/src/DesignationContract.ts @@ -0,0 +1,69 @@ +import { NativeContract } from './NativeContract'; +import { + NativeContractStorageContext, + utils, + StackItem, + assertArrayStackItem, + DesignationRole as Role, +} from '@neo-one/node-core'; +import { map, toArray } from 'rxjs/operators'; +import { ECPoint, common } from '@neo-one/client-common'; + +export class DesignationContract extends NativeContract { + public constructor() { + super({ + id: -5, + name: 'DesignationContract', + }); + } + + /** + * passing in height and index is a pretty HMMM way to do this but in the vein of being + * consistent with C# code as much as possible we will do it like this. The reasoning + * being that our snapshot equivalent 'storage' doesn't have knowledge of the current height, + * that is a blockchain abstraction. In almost no situation should this first error actually throw. + */ + public async getDesignatedByRole( + { storages }: NativeContractStorageContext, + role: Role, + height: number, + index: number, + ): Promise { + if (height + 1 < index) { + // TODO: implement makeError + throw new Error(`index: ${index} out of range for getDesignatedByRole.`); + } + + const key = this.createStorageKey(Buffer.from([role])) + .addUInt32BE(index) + .toSearchPrefix(); + const boundary = this.createStorageKey(Buffer.from([role])).toSearchPrefix(); + + const range = await storages + .find$(boundary, key) + .pipe( + map(({ value }) => utils.getInteroperable(value, NodeList.fromStackItem).members), + toArray(), + ) + .toPromise(); + + const publicKeys = range.length === 0 ? undefined : range[range.length - 1]; + + return publicKeys ?? []; + } +} + +class NodeList { + public static fromStackItem(stackItem: StackItem): NodeList { + const arrayItem = assertArrayStackItem(stackItem); + const members = arrayItem.array.map((item) => common.bufferToECPoint(item.getBuffer())); + + return new NodeList(members); + } + + public readonly members: readonly ECPoint[]; + + public constructor(members: readonly ECPoint[]) { + this.members = members; + } +} diff --git a/packages/neo-one-node-native/src/GASToken.ts b/packages/neo-one-node-native/src/GASToken.ts index 64ac350091..b67ec8b229 100644 --- a/packages/neo-one-node-native/src/GASToken.ts +++ b/packages/neo-one-node-native/src/GASToken.ts @@ -1,12 +1,12 @@ -import { NEP5NativeContract } from './Nep5'; +import { NEP17NativeContract } from './Nep17'; -export class GASToken extends NEP5NativeContract { +export class GASToken extends NEP17NativeContract { public static readonly decimals: number = 8; public constructor() { super({ id: -2, - name: 'GAS', - symbol: 'gas', + name: 'GasToken', + symbol: 'GAS', decimals: 8, }); } diff --git a/packages/neo-one-node-native/src/KeyBuilder.ts b/packages/neo-one-node-native/src/KeyBuilder.ts index 03750d6348..c06eb9ebd6 100644 --- a/packages/neo-one-node-native/src/KeyBuilder.ts +++ b/packages/neo-one-node-native/src/KeyBuilder.ts @@ -1,4 +1,5 @@ import { SerializableWire, StorageKey } from '@neo-one/node-core'; +import { BN } from 'bn.js'; export class KeyBuilder { private readonly id: number; @@ -26,6 +27,19 @@ export class KeyBuilder { return this; } + public addUInt32BE(value: number): this { + const buffer = Buffer.alloc(4); + buffer.writeUInt32BE(value); + + return this.addBuffer(buffer); + } + + public addUInt64LE(value: BN): this { + const buffer = value.toArrayLike(Buffer, 'le', 8); + + return this.addBuffer(buffer); + } + public toSearchPrefix(): Buffer { return StorageKey.createSearchPrefix(this.id, this.mutableBuffer); } diff --git a/packages/neo-one-node-native/src/ManagementContract.ts b/packages/neo-one-node-native/src/ManagementContract.ts new file mode 100644 index 0000000000..b7aaa0d742 --- /dev/null +++ b/packages/neo-one-node-native/src/ManagementContract.ts @@ -0,0 +1,43 @@ +import { UInt160 } from '@neo-one/client-common'; +import { ContractState, NativeContractStorageContext, utils } from '@neo-one/node-core'; +import { map, toArray } from 'rxjs/operators'; +import { NativeContract } from './NativeContract'; + +export class ManagementContract extends NativeContract { + private readonly prefixes = { + minimumDeploymentFee: Buffer.from([20]), + nextAvailableId: Buffer.from([15]), + contract: Buffer.from([8]), + }; + + public constructor() { + super({ + id: 0, + name: 'ManagementContract', + }); + } + + public async getContract({ storages }: NativeContractStorageContext, hash: UInt160) { + const maybeContract = await storages.tryGet( + this.createStorageKey(this.prefixes.contract).addBuffer(hash).toStorageKey(), + ); + + if (maybeContract === undefined) { + return undefined; + } + + return utils.getInteroperable(maybeContract, ContractState.fromStackItem); + } + + public async listContracts({ storages }: NativeContractStorageContext) { + const searchPrefix = this.createStorageKey(this.prefixes.contract).toSearchPrefix(); + + return storages + .find$(searchPrefix) + .pipe( + map(({ value }) => utils.getInteroperable(value, ContractState.fromStackItem)), + toArray(), + ) + .toPromise(); + } +} diff --git a/packages/neo-one-node-native/src/NEOToken.ts b/packages/neo-one-node-native/src/NEOToken.ts index 426b0f1887..0d11c4e309 100644 --- a/packages/neo-one-node-native/src/NEOToken.ts +++ b/packages/neo-one-node-native/src/NEOToken.ts @@ -1,13 +1,37 @@ -import { common, crypto, ECPoint, UInt160 } from '@neo-one/client-common'; +import { common, crypto, ECPoint, InvalidFormatError, UInt160 } from '@neo-one/client-common'; import { BlockchainSettings, Candidate, NativeContractStorageContext, utils } from '@neo-one/node-core'; import { BN } from 'bn.js'; import _ from 'lodash'; import { filter, map, toArray } from 'rxjs/operators'; import { CandidateState, NEOAccountState } from './AccountStates'; -import { GASToken } from './GASToken'; -import { NEP5NativeContract } from './Nep5'; +import { CachedCommittee } from './CachedCommittee'; +import { NEP17NativeContract } from './Nep17'; -export class NEOToken extends NEP5NativeContract { +type Storages = NativeContractStorageContext['storages']; + +interface CalculateBonusOptions { + readonly storages: Storages; + readonly vote?: ECPoint; + readonly value: BN; + readonly start: number; + readonly end: number; +} + +interface GasRecord { + readonly index: number; + readonly gasPerBlock: BN; +} + +const candidateSort = (a: Candidate, b: Candidate) => { + const voteComp = a.votes.cmp(b.votes); + if (voteComp !== 0) { + return voteComp; + } + + return a.publicKey.compare(b.publicKey); +}; + +export class NEOToken extends NEP17NativeContract { public readonly totalAmount: BN; // TODO: investigate this usage, its a strange decimal value in C# world. `0.2M`. Something to do with rounding. public readonly effectiveVoterTurnout = 0.2; @@ -16,14 +40,22 @@ export class NEOToken extends NEP5NativeContract { private readonly prefixes = { votersCount: Buffer.from([1]), candidate: Buffer.from([33]), - nextValidators: Buffer.from([14]), + committee: Buffer.from([14]), + gasPerBlock: Buffer.from([29]), + voterRewardPerCommittee: Buffer.from([23]), + }; + + private readonly ratios = { + neoHolderReward: 10, + committeeReward: 10, + voterReward: 10, }; public constructor(settings: BlockchainSettings) { super({ id: -1, - name: 'NEO', - symbol: 'neo', + name: 'NeoToken', + symbol: 'NEO', decimals: 0, }); @@ -46,108 +78,145 @@ export class NEOToken extends NEP5NativeContract { state: utils.getInteroperable(value, CandidateState.fromStackItem), })), filter((value) => value.state.registered), - // tslint:disable-next-line: no-useless-cast map(({ point, state }) => ({ publicKey: point, votes: state.votes })), toArray(), ) .toPromise(); } - public async getValidators(storage: NativeContractStorageContext): Promise { - const members = await this.getCommitteeMembers(storage); - - return members.slice(0, this.settings.validatorsCount).sort(common.ecPointCompare); - } - public async getCommittee(storage: NativeContractStorageContext): Promise { - // tslint:disable-next-line: prefer-immediate-return - const members = await this.getCommitteeMembers(storage); + const cache = await this.getCommitteeFromCache(storage); - return members.slice().sort(common.ecPointCompare); + return cache.members.map(({ publicKey }) => publicKey).sort(common.ecPointCompare); } public async getCommitteeAddress(storage: NativeContractStorageContext): Promise { - const committees = await this.getCommittee(storage); + const committee = await this.getCommittee(storage); return crypto.toScriptHash( - crypto.createMultiSignatureRedeemScript(committees.length - (committees.length - 1) / 2, committees), + crypto.createMultiSignatureRedeemScript(committee.length - (committee.length - 1) / 2, committee), ); } - public async unclaimedGas({ storages }: NativeContractStorageContext, account: UInt160, end: number) { - const storage = await storages.tryGet(this.createStorageKey(this.accountPrefix).addBuffer(account).toStorageKey()); - if (storage === undefined) { - return new BN(0); - } + public async getCommitteeFromCache({ storages }: NativeContractStorageContext): Promise { + const item = await storages.get(this.createStorageKey(this.prefixes.committee).toStorageKey()); - const state = utils.getInteroperable(storage, NEOAccountState.fromStackItem); + return utils.getInteroperable(item, CachedCommittee.fromStackItem); + } + + public async computeNextBlockValidators(storage: NativeContractStorageContext): Promise { + const committeeMembers = await this.computeCommitteeMembers(storage); - return this.calculateBonus(state.balance, state.balanceHeight.toNumber(), end); + return _.take( + committeeMembers.map(({ publicKey }) => publicKey), + this.settings.validatorsCount, + ) + .slice() + .sort(common.ecPointCompare); } - public async getNextBlockValidators({ storages }: NativeContractStorageContext): Promise { - const key = this.createStorageKey(this.prefixes.nextValidators).toStorageKey(); - const storage = await storages.tryGet(key); + public async unclaimedGas({ storages }: NativeContractStorageContext, account: UInt160, end: number) { + const storage = await storages.tryGet( + this.createStorageKey(this.basePrefixes.account).addBuffer(account).toStorageKey(), + ); if (storage === undefined) { - return this.settings.standbyValidators; + return new BN(0); } - return utils.getSerializableArrayFromStorageItem(storage, (reader) => reader.readECPoint()); - } + const state = utils.getInteroperable(storage, NEOAccountState.fromStackItem); - private async getCommitteeMembers(storage: NativeContractStorageContext): Promise { - const item = await storage.storages.get(this.createStorageKey(this.prefixes.votersCount).toStorageKey()); - const votersCount = new BN(item.value, 'le').toNumber(); - const voterTurnout = votersCount / this.totalAmount.toNumber(); - if (voterTurnout < this.effectiveVoterTurnout) { - return this.settings.standbyCommittee; - } + return this.calculateBonus({ + storages, + vote: state.voteTo, + value: state.balance, + start: state.balanceHeight.toNumber(), + end, + }); + } - const candidates = await this.getCandidates(storage); - if (candidates.length < this.settings.committeeMembersCount) { - return this.settings.standbyCommittee; - } + public async getNextBlockValidators(storage: NativeContractStorageContext): Promise { + const committeeCache = await this.getCommitteeFromCache(storage); - return _.sortBy(candidates, ['votes', ({ publicKey }) => common.ecPointToHex(publicKey)]) + return _.take(committeeCache.members, this.settings.validatorsCount) .map(({ publicKey }) => publicKey) - .slice(0, this.settings.committeeMembersCount); + .sort(common.ecPointCompare); } - private calculateBonus(value: BN, start: number, end: number) { + private async calculateBonus({ storages, vote, value, start, end }: CalculateBonusOptions) { if (value.isZero() || start >= end) { return new BN(0); } if (value.ltn(0)) { - // TODO: create a real error for here - throw new Error('negative value not supported'); + throw new InvalidFormatError('negative value not supported'); } - let amount = new BN(0); - let ustart = Math.floor(start / this.settings.decrementInterval); - if (ustart < this.settings.generationAmount.length) { - let istart = start % this.settings.decrementInterval; - let uend = Math.floor(end / this.settings.decrementInterval); - let iend = end % this.settings.decrementInterval; - if (uend >= this.settings.generationAmount.length) { - uend = this.settings.generationAmount.length; - iend = 0; - } - if (iend === 0) { - uend -= 1; - iend = this.settings.decrementInterval; - } - // tslint:disable-next-line: no-loop-statement - while (ustart < uend) { - amount = amount.addn((this.settings.decrementInterval - istart) * this.settings.generationAmount[ustart]); - ustart += 1; - istart = 0; + const neoHolderReward = await this.calculateNeoHolderReward(storages, value, start, end); + if (vote === undefined) { + return neoHolderReward; + } + + const border = this.createStorageKey(this.prefixes.voterRewardPerCommittee).addBuffer(vote).toSearchPrefix(); + const keyStart = this.createStorageKey(this.prefixes.voterRewardPerCommittee) + .addBuffer(vote) + .addUInt32BE(start) + .toSearchPrefix(); + const startRange = await storages.find$(border, keyStart).pipe(toArray()).toPromise(); + const startItem = startRange.length === 0 ? undefined : startRange[startRange.length - 1].value; + const startRewardPerNeo = startItem === undefined ? new BN(0) : new BN(startItem.value, 'le'); + + const keyEnd = this.createStorageKey(this.prefixes.voterRewardPerCommittee) + .addBuffer(vote) + .addUInt32BE(end) + .toSearchPrefix(); + const endRange = await storages.find$(border, keyEnd).pipe(toArray()).toPromise(); + const endItem = endRange.length === 0 ? undefined : endRange[endRange.length - 1].value; + const endRewardPerNeo = endItem === undefined ? new BN(0) : new BN(endItem.value, 'le'); + + return neoHolderReward.add(value.mul(endRewardPerNeo.sub(startRewardPerNeo)).div(this.totalAmount)); + } + + private async calculateNeoHolderReward(storages: Storages, value: BN, start: number, end: number) { + let sum = new BN(0); + const sortedGasRecords = await this.getSortedGasRecords(storages, end); + // tslint:disable-next-line: no-loop-statement + for (const { index, gasPerBlock } of sortedGasRecords) { + if (index > start) { + sum = sum.add(gasPerBlock.muln(end - index)); + } else { + sum = sum.add(gasPerBlock.muln(end - index)); + break; } - amount = amount.addn((iend - istart) * this.settings.generationAmount[ustart]); } - return common - .fixedFromDecimal(amount.mul(value), GASToken.decimals) - .mul(new BN(10 ** GASToken.decimals)) - .div(this.totalAmount); + return value.mul(sum).muln(this.ratios.neoHolderReward).divn(100).div(this.totalAmount); + } + + private async getSortedGasRecords(storages: Storages, end: number): Promise { + const prefix = this.createStorageKey(this.prefixes.gasPerBlock).addUInt32BE(end).toSearchPrefix(); + const boundary = this.createStorageKey(this.prefixes.gasPerBlock).toSearchPrefix(); + const range = await storages.find$(prefix, boundary).pipe(toArray()).toPromise(); + + return range + .map(({ key, value }) => ({ + index: key.key.readUInt32BE(4), + gasPerBlock: new BN(value.value, 'le'), + })) + .reverse(); + } + + private async computeCommitteeMembers({ storages }: NativeContractStorageContext): Promise { + const item = await storages.get(this.createStorageKey(this.prefixes.votersCount).toStorageKey()); + const votersCount = new BN(item.value).toNumber(); + const voterTurnout = votersCount / this.totalAmount.toNumber(); + const candidates = await this.getCandidates({ storages }); + + if (voterTurnout < this.effectiveVoterTurnout || candidates.length < this.settings.committeeMembersCount) { + return this.settings.standbyCommittee.map((member) => ({ + publicKey: member, + votes: candidates.find((candidate) => candidate.publicKey.equals(member))?.votes ?? new BN(0), + })); + } + + return _.take(candidates.slice().sort(candidateSort), this.settings.committeeMembersCount); } } diff --git a/packages/neo-one-node-native/src/NativeContainer.ts b/packages/neo-one-node-native/src/NativeContainer.ts index 3f17aa255e..144de0a2cd 100644 --- a/packages/neo-one-node-native/src/NativeContainer.ts +++ b/packages/neo-one-node-native/src/NativeContainer.ts @@ -1,16 +1,39 @@ +import { UInt160 } from '@neo-one/client-common'; import { BlockchainSettings } from '@neo-one/node-core'; +import { DesignationContract } from './DesignationContract'; import { GASToken } from './GASToken'; +import { ManagementContract } from './ManagementContract'; import { NEOToken } from './NEOToken'; +import { OracleContract } from './OracleContract'; import { PolicyContract } from './Policy'; export class NativeContainer { + public readonly Management: ManagementContract; public readonly NEO: NEOToken; public readonly GAS: GASToken; public readonly Policy: PolicyContract; + public readonly Oracle: OracleContract; + public readonly Designation: DesignationContract; + public readonly nativeHashes: readonly UInt160[]; public constructor(settings: BlockchainSettings) { + this.Management = new ManagementContract(); this.NEO = new NEOToken(settings); this.GAS = new GASToken(); this.Policy = new PolicyContract(); + this.Oracle = new OracleContract(); + this.Designation = new DesignationContract(); + this.nativeHashes = [ + this.Management.hash, + this.NEO.hash, + this.GAS.hash, + this.Policy.hash, + this.Oracle.hash, + this.Designation.hash, + ]; + } + + public isNative(hash: UInt160) { + return this.nativeHashes.some((nativeHash) => hash.equals(nativeHash)); } } diff --git a/packages/neo-one-node-native/src/NativeContract.ts b/packages/neo-one-node-native/src/NativeContract.ts index 363d4ea68d..8b46fc560c 100644 --- a/packages/neo-one-node-native/src/NativeContract.ts +++ b/packages/neo-one-node-native/src/NativeContract.ts @@ -1,4 +1,4 @@ -import { crypto, ScriptBuilder, UInt160 } from '@neo-one/client-common'; +import { common, crypto, ScriptBuilder, UInt160 } from '@neo-one/client-common'; import { KeyBuilder } from './KeyBuilder'; export interface NativeContractAdd { @@ -11,6 +11,8 @@ export abstract class NativeContract { public readonly script: Buffer; public readonly hash: UInt160; public readonly id: number; + // newly added property will see if it is relevant on our end + // public readonly activeBlockIndex: number; public constructor({ name, id }: NativeContractAdd) { this.name = name; @@ -18,10 +20,10 @@ export abstract class NativeContract { const builder = new ScriptBuilder(); builder.emitPushString(this.name); - builder.emitSysCall('Neo.Native.Call'); + builder.emitSysCall('System.Contract.CallNative'); this.script = builder.build(); - this.hash = crypto.toScriptHash(this.script); + this.hash = crypto.getContractHash(common.ZERO_UINT160, this.script); } protected createStorageKey(prefix: Buffer) { diff --git a/packages/neo-one-node-native/src/Nep5.ts b/packages/neo-one-node-native/src/Nep17.ts similarity index 66% rename from packages/neo-one-node-native/src/Nep5.ts rename to packages/neo-one-node-native/src/Nep17.ts index 642c6fbcb8..c653725c41 100644 --- a/packages/neo-one-node-native/src/Nep5.ts +++ b/packages/neo-one-node-native/src/Nep17.ts @@ -3,20 +3,22 @@ import { NativeContractStorageContext } from '@neo-one/node-core'; import { BN } from 'bn.js'; import { NativeContract, NativeContractAdd } from './NativeContract'; -export interface NEP5NativeContractAdd extends NativeContractAdd { +export interface NEP17NativeContractAdd extends NativeContractAdd { readonly symbol: string; readonly decimals: number; } -export abstract class NEP5NativeContract extends NativeContract { +export abstract class NEP17NativeContract extends NativeContract { public readonly symbol: string; public readonly decimals: number; public readonly factor: BN; - protected readonly totalSupplyPrefix = Buffer.from([11]); - protected readonly accountPrefix = Buffer.from([20]); + protected readonly basePrefixes = { + totalSupply: Buffer.from([11]), + account: Buffer.from([20]), + }; - public constructor(options: NEP5NativeContractAdd) { + public constructor(options: NEP17NativeContractAdd) { super(options); this.symbol = options.symbol; this.decimals = options.decimals; @@ -24,7 +26,7 @@ export abstract class NEP5NativeContract extends NativeContract { } public async totalSupply({ storages }: NativeContractStorageContext): Promise { - const storage = await storages.tryGet(this.createStorageKey(this.totalSupplyPrefix).toStorageKey()); + const storage = await storages.tryGet(this.createStorageKey(this.basePrefixes.totalSupply).toStorageKey()); if (storage === undefined) { return new BN(0); } @@ -33,7 +35,9 @@ export abstract class NEP5NativeContract extends NativeContract { } public async balanceOf({ storages }: NativeContractStorageContext, account: UInt160): Promise { - const storage = await storages.tryGet(this.createStorageKey(this.accountPrefix).addBuffer(account).toStorageKey()); + const storage = await storages.tryGet( + this.createStorageKey(this.basePrefixes.totalSupply).addBuffer(account).toStorageKey(), + ); if (storage === undefined) { return new BN(0); } diff --git a/packages/neo-one-node-native/src/OracleContract.ts b/packages/neo-one-node-native/src/OracleContract.ts new file mode 100644 index 0000000000..0f42a77488 --- /dev/null +++ b/packages/neo-one-node-native/src/OracleContract.ts @@ -0,0 +1,96 @@ +import { crypto } from '@neo-one/client-common'; +import { + assertArrayStackItem, + NativeContractStorageContext, + OracleRequest, + OracleRequestResults, + StackItem, + utils, +} from '@neo-one/node-core'; +import { BN } from 'bn.js'; +import { map, toArray } from 'rxjs/operators'; +import { NativeContract } from './NativeContract'; + +export class OracleContract extends NativeContract { + private readonly prefixes = { + requestId: Buffer.from([9]), + request: Buffer.from([7]), + idList: Buffer.from([6]), + }; + + // applicationEngine constants that might be used later + // private maxUrlLength = 256 as const; + // private maxFilterLength = 128 as const; + // private maxCallbackLength = 32 as const; + // private maxUserDataLength = 512 as const; + // private oracleRequestPrice = common.fixed8FromDecimal('.5'); + + public constructor() { + super({ + id: -4, + name: 'OracleContract', + }); + } + + public async getRequest({ storages }: NativeContractStorageContext, id: BN) { + const item = await storages.tryGet(this.createStorageKey(this.prefixes.request).addUInt64LE(id).toStorageKey()); + + if (item === undefined) { + return undefined; + } + + return utils.getInteroperable(item, OracleRequest.fromStackItem); + } + + public async getRequests({ storages }: NativeContractStorageContext): Promise { + return storages + .find$(this.createStorageKey(this.prefixes.request).toSearchPrefix()) + .pipe( + map( + ({ key, value }) => + // tslint:disable-next-line: no-useless-cast + [new BN(key.key.slice(1), 'le'), utils.getInteroperable(value, OracleRequest.fromStackItem)] as const, + ), + toArray(), + ) + .toPromise(); + } + + public async getRequestsByUrl({ storages }: NativeContractStorageContext, url: string) { + const maybeListItem = await storages.tryGet( + this.createStorageKey(this.prefixes.idList).addBuffer(this.getUrlHash(url)).toStorageKey(), + ); + if (maybeListItem === undefined) { + return []; + } + + const { list } = utils.getInteroperable(maybeListItem, IdList.fromStackItem); + + return Promise.all( + list.map(async (id) => { + const request = await storages.get(this.createStorageKey(this.prefixes.request).addUInt64LE(id).toStorageKey()); + + return utils.getInteroperable(request, OracleRequest.fromStackItem); + }), + ); + } + + private getUrlHash(url: string) { + return crypto.hash160(Buffer.from(url, 'utf8')); + } +} + +class IdList { + public static fromStackItem(stackItem: StackItem): IdList { + const { array } = assertArrayStackItem(stackItem); + const list = array.map((item) => item.getInteger()); + + return new IdList(list); + } + + public readonly list: readonly BN[]; + + public constructor(list: readonly BN[]) { + this.list = list; + } +} diff --git a/packages/neo-one-node-native/src/Policy.ts b/packages/neo-one-node-native/src/Policy.ts index 9951682782..1fcd3fc696 100644 --- a/packages/neo-one-node-native/src/Policy.ts +++ b/packages/neo-one-node-native/src/Policy.ts @@ -1,5 +1,5 @@ import { common, UInt160 } from '@neo-one/client-common'; -import { NativeContractStorageContext, utils } from '@neo-one/node-core'; +import { NativeContractStorageContext } from '@neo-one/node-core'; import { BN } from 'bn.js'; import { GASToken } from './GASToken'; import { NativeContract } from './NativeContract'; @@ -9,15 +9,20 @@ export class PolicyContract extends NativeContract { private readonly prefixes = { maxTransactionsPerBlock: Buffer.from([23]), feePerByte: Buffer.from([10]), - blockedAccounts: Buffer.from([15]), + blockedAccount: Buffer.from([15]), maxBlockSize: Buffer.from([12]), maxBlockSystemFee: Buffer.from([17]), + execFeeFactor: Buffer.from([18]), + storagePrice: Buffer.from([19]), }; + private readonly defaultExecFeeFactor = 30; + private readonly defaultStoragePrice = 100000; + public constructor() { super({ id: -3, - name: 'Policy', + name: 'PolicyContract', }); } @@ -57,12 +62,29 @@ export class PolicyContract extends NativeContract { return new BN(item.value); } - public async getBlockedAccounts({ storages }: NativeContractStorageContext): Promise { - const item = await storages.tryGet(this.createStorageKey(this.prefixes.blockedAccounts).toStorageKey()); - if (item !== undefined) { - return utils.getSerializableArrayFromStorageItem(item, (reader) => reader.readUInt160()); + public async getExecFeeFactor({ storages }: NativeContractStorageContext) { + const item = await storages.tryGet(this.createStorageKey(this.prefixes.execFeeFactor).toStorageKey()); + if (item === undefined) { + return this.defaultExecFeeFactor; + } + + return new BN(item.value).toNumber(); + } + + public async getStoragePrice({ storages }: NativeContractStorageContext) { + const item = await storages.tryGet(this.createStorageKey(this.prefixes.storagePrice).toStorageKey()); + if (item === undefined) { + return this.defaultStoragePrice; } - return []; + return new BN(item.value).toNumber(); + } + + public async isBlocked({ storages }: NativeContractStorageContext, account: UInt160) { + const item = await storages.tryGet( + this.createStorageKey(this.prefixes.blockedAccount).addBuffer(account).toStorageKey(), + ); + + return item !== undefined; } } diff --git a/packages/neo-one-node-native/src/index.ts b/packages/neo-one-node-native/src/index.ts index 5a77ae007d..0c91f79dd7 100644 --- a/packages/neo-one-node-native/src/index.ts +++ b/packages/neo-one-node-native/src/index.ts @@ -4,5 +4,5 @@ export * from './KeyBuilder'; export * from './NativeContainer'; export * from './NativeContract'; export * from './NEOToken'; -export * from './Nep5'; +export * from './Nep17'; export * from './Policy'; diff --git a/packages/neo-one-node-protocol/src/Message.ts b/packages/neo-one-node-protocol/src/Message.ts index 735106bd7e..17da7813d8 100644 --- a/packages/neo-one-node-protocol/src/Message.ts +++ b/packages/neo-one-node-protocol/src/Message.ts @@ -32,6 +32,19 @@ import { VersionPayload, } from './payload'; +const tryCompression = ({ command }: MessageValue) => { + return ( + command === Command.Block || + command === Command.Consensus || + command === Command.Transaction || + command === Command.Headers || + command === Command.Addr || + command === Command.MerkleBlock || + command === Command.FilterLoad || + command === Command.FilterAdd + ); +}; + export type MessageValue = | { readonly command: Command.Addr; readonly payload: AddrPayload } | { readonly command: Command.Block; readonly payload: Block } @@ -188,7 +201,7 @@ export class Message implements SerializableWire { public static create(value: MessageValue): Message { // tslint:disable-next-line: no-any const payloadBuffer = (value as any)?.payload?.serializeWire() ?? Buffer.alloc(0); - if (payloadBuffer.length > compressionMinSize) { + if (tryCompression(value) && payloadBuffer.length > compressionMinSize) { const compressed = lz4Helper.compress(payloadBuffer); if (compressed.length < payloadBuffer.length - compressionThreshold) { return new Message({ diff --git a/packages/neo-one-node-protocol/src/Node.ts b/packages/neo-one-node-protocol/src/Node.ts index 43d5f16961..326ecab1e8 100644 --- a/packages/neo-one-node-protocol/src/Node.ts +++ b/packages/neo-one-node-protocol/src/Node.ts @@ -135,11 +135,6 @@ const compareTransactionAndFees = (val1: TransactionAndFee, val2: TransactionAnd return val1.transaction.hash.compare(val2.transaction.hash); }; -// TODO: We should note what some of these settings used to be, I've made them more aggressive -// while we are testing syncing; and then testnet can be a bit slow. -// const GET_BLOCKS_THROTTLE_MS = 1000; -// const GET_BLOCKS_TIME_MS = 5000; - const MEM_POOL_SIZE = 5000; const GET_BLOCKS_COUNT = 500; // Assume that we get 500 back, but if not, at least request every 10 seconds @@ -162,6 +157,8 @@ export class Node implements INode { public readonly getNewVerificationContext: () => TransactionVerificationContext; // tslint:disable-next-line readonly-keyword private mutableMemPool: { [hash: string]: Transaction }; + // tslint:disable-next-line: readonly-keyword + private readonly mutableSentCommands: { [k: number]: boolean } = {}; private readonly transactionVerificationContext: TransactionVerificationContext; private readonly network: Network; private readonly options: Options; @@ -498,6 +495,7 @@ export class Node implements INode { private sendMessage(peer: Peer | ConnectedPeer, message: Message): void { peer.write(message.serializeWire()); + this.mutableSentCommands[message.value.command] = true; } private readonly negotiate = async (peer: Peer): Promise> => { this.sendMessage( @@ -847,6 +845,11 @@ export class Node implements INode { } private onAddrMessageReceived(addr: AddrPayload): void { + if (!this.mutableSentCommands[Command.GetAddr]) { + return; + } + this.mutableSentCommands[Command.GetAddr] = false; + addr.addressList .filter((address) => !LOCAL_HOST_ADDRESSES.has(address.address)) .filter((address) => address.port > 0) diff --git a/packages/neo-one-node-rpc-handler/src/createHandler.ts b/packages/neo-one-node-rpc-handler/src/createHandler.ts index 35324bebd5..f30b0d13e8 100644 --- a/packages/neo-one-node-rpc-handler/src/createHandler.ts +++ b/packages/neo-one-node-rpc-handler/src/createHandler.ts @@ -9,7 +9,6 @@ import { toVMStateJSON, TransactionJSON, TransactionReceiptJSON, - utils as commonUtils, VerboseTransactionJSON, VerifyResultModel, } from '@neo-one/client-common'; @@ -23,7 +22,6 @@ import { Nep5Transfer, Nep5TransferKey, Node, - Signer, Signers, StackItem, stackItemToJSON, @@ -125,6 +123,7 @@ const RPC_METHODS: { readonly [key: string]: string } = { // NEO•ONE getfeeperbyte: 'getfeeperbyte', + getexecfeefactor: 'getexecfeefactor', getverificationcost: 'getverificationcost', relaytransaction: 'relaytransaction', getallstorage: 'getallstorage', @@ -306,6 +305,7 @@ export const createHandler = ({ try { const stack = stackIn.map((item: StackItem) => stackItemToJSON(item, undefined)); + return { script: script.toString('hex'), state: toVMStateJSON(state), @@ -481,7 +481,7 @@ export const createHandler = ({ }, [RPC_METHODS.getvalidators]: async () => { const [validators, candidates] = await Promise.all([ - native.NEO.getValidators({ storages: blockchain.storages }), + native.NEO.computeNextBlockValidators({ storages: blockchain.storages }), native.NEO.getCandidates({ storages: blockchain.storages }), ]); @@ -886,6 +886,7 @@ export const createHandler = ({ return feePerByte.toString(); }, + [RPC_METHODS.getexecfeefactor]: async () => native.Policy.getExecFeeFactor({ storages: blockchain.storages }), [RPC_METHODS.getverificationcost]: async (args) => { const hash = JSONHelper.readUInt160(args[0]); const transaction = Transaction.deserializeWire({ diff --git a/packages/neo-one-node-vm/lib/Dispatcher.Engine.cs b/packages/neo-one-node-vm/lib/Dispatcher.Engine.cs index cae8b20e56..682f00b790 100644 --- a/packages/neo-one-node-vm/lib/Dispatcher.Engine.cs +++ b/packages/neo-one-node-vm/lib/Dispatcher.Engine.cs @@ -6,6 +6,7 @@ using Neo.Network.P2P.Payloads; using Neo.Persistence; using Neo.SmartContract; +using Neo.SmartContract.Native; using Neo.VM; using Neo.VM.Types; @@ -66,6 +67,7 @@ private enum EngineMethod create, execute, loadscript, + loadcontract, setinstructionpointer, getvmstate, getresultstack, @@ -111,6 +113,14 @@ private dynamic dispatchEngineMethod(EngineMethod method, dynamic args) return this._loadScript(script, flags, scriptHash, initialPosition); + case EngineMethod.loadcontract: + UInt160 contractHash = new UInt160((byte[])args.hash); + string contractMethod = (string)args.method; + CallFlags contractFlags = (CallFlags)((byte)args.flags); + bool packParameters = (bool)args.packParameters; + + return this._loadContract(contractHash, contractMethod, contractFlags, packParameters); + case EngineMethod.getvmstate: return this._getVMState(); @@ -169,6 +179,17 @@ private bool _loadScript(Script script, CallFlags flags, UInt160 hash = null, in return true; } + private bool _loadContract(UInt160 hash, string method, CallFlags flags, bool packParameters) + { + this.isEngineInitialized(); + ContractState cs = NativeContract.Management.GetContract(this.snapshot, hash); + if (cs is null) return false; + + this.engine.LoadContract(cs, method, flags, packParameters); + + return true; + } + private VMState _getVMState() { return this.engine != null ? this.engine.State : VMState.BREAK; diff --git a/packages/neo-one-node-vm/lib/Dispatcher.cs b/packages/neo-one-node-vm/lib/Dispatcher.cs index 99f1b45c46..7c327baa8e 100644 --- a/packages/neo-one-node-vm/lib/Dispatcher.cs +++ b/packages/neo-one-node-vm/lib/Dispatcher.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using Neo.Persistence; using Microsoft.Extensions.Configuration; +using Neo.SmartContract.Native; namespace NEOONE { @@ -124,7 +125,7 @@ private bool _dispose() private dynamic _test() { - return this.snapshot.PersistingBlock.Index.ToString(); + return NativeContract.NEO.Name; } private NEOONE.ReturnHelpers.ProtocolSettingsReturn _getConfig() diff --git a/packages/neo-one-node-vm/src/ApplicationEngine.ts b/packages/neo-one-node-vm/src/ApplicationEngine.ts index 532079db48..839e6aff19 100644 --- a/packages/neo-one-node-vm/src/ApplicationEngine.ts +++ b/packages/neo-one-node-vm/src/ApplicationEngine.ts @@ -5,6 +5,7 @@ import { serializeScriptContainer, SnapshotName, LoadScriptOptions, + LoadContractOptions, } from '@neo-one/node-core'; import { BN } from 'bn.js'; import _ from 'lodash'; @@ -114,6 +115,18 @@ export class ApplicationEngine { }); } + public loadContract({ hash, method, flags, packParameters = false }: LoadContractOptions) { + return this.dispatch({ + method: 'loadcontract', + args: { + hash, + method, + flags, + packParameters, + }, + }); + } + public checkScript() { return this.dispatch({ method: 'checkscript', diff --git a/packages/neo-one-node-vm/src/Methods/EngineMethods.ts b/packages/neo-one-node-vm/src/Methods/EngineMethods.ts index af2edfc2f9..f75a4aadbc 100644 --- a/packages/neo-one-node-vm/src/Methods/EngineMethods.ts +++ b/packages/neo-one-node-vm/src/Methods/EngineMethods.ts @@ -1,5 +1,5 @@ import { TriggerType, VMState, UInt160Hex } from '@neo-one/client-common'; -import { CallFlags, SerializedScriptContainer, SnapshotName, LoadScriptOptions } from '@neo-one/node-core'; +import { CallFlags, SerializedScriptContainer, SnapshotName, LoadContractOptions } from '@neo-one/node-core'; import { StackItemReturn, LogReturn } from '../converters'; import { DefaultMethods, DispatchMethod } from '../types'; @@ -30,4 +30,5 @@ export interface EngineMethods extends DefaultMethods { // methods readonly execute: DispatchMethod; readonly loadscript: DispatchMethod; + readonly loadcontract: DispatchMethod; } diff --git a/packages/neo-one-node-vm/src/__tests__/Dispatcher.test.ts b/packages/neo-one-node-vm/src/__tests__/Dispatcher.test.ts index c9c6c127b4..39398edc01 100644 --- a/packages/neo-one-node-vm/src/__tests__/Dispatcher.test.ts +++ b/packages/neo-one-node-vm/src/__tests__/Dispatcher.test.ts @@ -101,4 +101,8 @@ describe('Dispatcher Tests', () => { test('Dispatcher returns config without initializing config', () => { expect(dispatcher.getConfig()).toBeDefined(); }); + + test.only('test output only', () => { + console.log(dispatcher.test()); + }); });