From 393479ea69d845fe8793a970921720e6c848d7a2 Mon Sep 17 00:00:00 2001 From: Aayush Date: Tue, 15 Feb 2022 18:00:39 -0500 Subject: [PATCH 1/2] Fvm: impl VerifyConsensusFault --- chain/vm/fvm.go | 203 +++++++++++++++++++++++++++++++++++++++++++- extern/filecoin-ffi | 2 +- 2 files changed, 200 insertions(+), 5 deletions(-) diff --git a/chain/vm/fvm.go b/chain/vm/fvm.go index 1e394a20258..f98c043dd69 100644 --- a/chain/vm/fvm.go +++ b/chain/vm/fvm.go @@ -1,10 +1,16 @@ package vm import ( + "bytes" "context" + "github.com/filecoin-project/lotus/chain/state" + cbor "github.com/ipfs/go-ipld-cbor" + + "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/exitcode" + "github.com/filecoin-project/lotus/lib/sigs" "golang.org/x/xerrors" @@ -13,20 +19,209 @@ import ( ffi "github.com/filecoin-project/filecoin-ffi" ffi_cgo "github.com/filecoin-project/filecoin-ffi/cgo" + "github.com/filecoin-project/lotus/chain/actors/adt" + "github.com/filecoin-project/lotus/chain/actors/builtin/miner" "github.com/filecoin-project/lotus/chain/types" "github.com/ipfs/go-cid" ) var _ VMI = (*FVM)(nil) +var _ ffi_cgo.Externs = (*FvmExtern)(nil) type FvmExtern struct { Rand blockstore.Blockstore + epoch abi.ChainEpoch + lbState LookbackStateGetter + base cid.Cid +} + +// Similar to the one in syscalls.go used by the Lotus VM, except it never errors +// Errors are logged and "no fault" is returned, which is functionally what go-actors does anyway +func (x *FvmExtern) VerifyConsensusFault(ctx context.Context, a, b, extra []byte) *ffi_cgo.ConsensusFaultWithGas { + ret := &ffi_cgo.ConsensusFaultWithGas{ + // TODO: is this gonna be a problem on the receiving end? should we return address.NewIDAddress(0) instead? + Target: address.Undef, + Type: ffi_cgo.ConsensusFaultNone, + } + + // Note that block syntax is not validated. Any validly signed block will be accepted pursuant to the below conditions. + // Whether or not it could ever have been accepted in a chain is not checked/does not matter here. + // for that reason when checking block parent relationships, rather than instantiating a Tipset to do so + // (which runs a syntactic check), we do it directly on the CIDs. + + // (0) cheap preliminary checks + + // can blocks be decoded properly? + var blockA, blockB types.BlockHeader + if decodeErr := blockA.UnmarshalCBOR(bytes.NewReader(a)); decodeErr != nil { + log.Info("invalid consensus fault: cannot decode first block header: %w", decodeErr) + return ret + } + + if decodeErr := blockB.UnmarshalCBOR(bytes.NewReader(b)); decodeErr != nil { + log.Info("invalid consensus fault: cannot decode second block header: %w", decodeErr) + return ret + } + + // Commented out from the Lotus VM code: FvmExtern only supports v14 and onwards + // workaround chain halt + //if build.IsNearUpgrade(blockA.Height, build.UpgradeOrangeHeight) { + // return nil, xerrors.Errorf("consensus reporting disabled around Upgrade Orange") + //} + //if build.IsNearUpgrade(blockB.Height, build.UpgradeOrangeHeight) { + // return nil, xerrors.Errorf("consensus reporting disabled around Upgrade Orange") + //} + + // are blocks the same? + if blockA.Cid().Equals(blockB.Cid()) { + log.Info("invalid consensus fault: submitted blocks are the same") + return ret + } + // (1) check conditions necessary to any consensus fault + + // were blocks mined by same miner? + if blockA.Miner != blockB.Miner { + log.Info("invalid consensus fault: blocks not mined by the same miner") + return ret + } + + ret.Target = blockA.Miner + + // block a must be earlier or equal to block b, epoch wise (ie at least as early in the chain). + if blockB.Height < blockA.Height { + log.Info("invalid consensus fault: first block must not be of higher height than second") + return ret + } + + ret.Epoch = blockB.Height + + faultType := ffi_cgo.ConsensusFaultNone + + // (2) check for the consensus faults themselves + // (a) double-fork mining fault + if blockA.Height == blockB.Height { + faultType = ffi_cgo.ConsensusFaultDoubleForkMining + } + + // (b) time-offset mining fault + // strictly speaking no need to compare heights based on double fork mining check above, + // but at same height this would be a different fault. + if types.CidArrsEqual(blockA.Parents, blockB.Parents) && blockA.Height != blockB.Height { + faultType = ffi_cgo.ConsensusFaultTimeOffsetMining + } + + // (c) parent-grinding fault + // Here extra is the "witness", a third block that shows the connection between A and B as + // A's sibling and B's parent. + // Specifically, since A is of lower height, it must be that B was mined omitting A from its tipset + // + // B + // | + // [A, C] + var blockC types.BlockHeader + if len(extra) > 0 { + if decodeErr := blockC.UnmarshalCBOR(bytes.NewReader(extra)); decodeErr != nil { + log.Info("invalid consensus fault: cannot decode extra: %w", decodeErr) + // just to match Lotus VM consensus, zero out any already-set faults + faultType = ffi_cgo.ConsensusFaultNone + return ret + } + + if types.CidArrsEqual(blockA.Parents, blockC.Parents) && blockA.Height == blockC.Height && + types.CidArrsContains(blockB.Parents, blockC.Cid()) && !types.CidArrsContains(blockB.Parents, blockA.Cid()) { + faultType = ffi_cgo.ConsensusFaultParentGrinding + } + } + + // (3) return if no consensus fault by now + if faultType == ffi_cgo.ConsensusFaultNone { + log.Info("invalid consensus fault: no fault detected") + return ret + } + + // else + // (4) expensive final checks + + // check blocks are properly signed by their respective miner + // note we do not need to check extra's: it is a parent to block b + // which itself is signed, so it was willingly included by the miner + gasUsed, sigErr := x.VerifyBlockSig(ctx, &blockA) + ret.GasUsed += gasUsed + if sigErr != nil { + log.Info("invalid consensus fault: cannot verify first block sig: %w", sigErr) + return ret + } + + gasUsed, sigErr = x.VerifyBlockSig(ctx, &blockB) + ret.GasUsed += gasUsed + if sigErr != nil { + log.Info("invalid consensus fault: cannot verify second block sig: %w", sigErr) + return ret + } + + ret.Type = faultType + + return ret +} + +func (x *FvmExtern) VerifyBlockSig(ctx context.Context, blk *types.BlockHeader) (int64, error) { + waddr, gasUsed, err := x.workerKeyAtLookback(ctx, blk.Miner, blk.Height) + if err != nil { + return gasUsed, err + } + + return gasUsed, sigs.CheckBlockSignature(ctx, blk, waddr) } -func (x *FvmExtern) VerifyConsensusFault(ctx context.Context, h1, h2, extra []byte) (*ffi_cgo.ConsensusFault, error) { - // TODO - panic("unimplemented") +func (x *FvmExtern) workerKeyAtLookback(ctx context.Context, minerId address.Address, height abi.ChainEpoch) (address.Address, int64, error) { + // Commented out from the Lotus VM code: FvmExtern only supports v14 and onwards + //if x.networkVersion >= network.Version7 && height < x.epoch-policy.ChainFinality { + // return address.Undef, xerrors.Errorf("cannot get worker key (currEpoch %d, height %d)", ss.epoch, height) + //} + + gasUsed := int64(0) + gasAdder := func(gc GasCharge) { + // technically not overflow safe, but that's fine + gasUsed += gc.Total() + } + + cstWithoutGas := cbor.NewCborStore(x.Blockstore) + cbb := &gasChargingBlocks{gasAdder, PricelistByEpoch(x.epoch), x.Blockstore} + cstWithGas := cbor.NewCborStore(cbb) + + lbState, err := x.lbState(ctx, height) + if err != nil { + return address.Undef, gasUsed, err + } + // get appropriate miner actor + act, err := lbState.GetActor(minerId) + if err != nil { + return address.Undef, gasUsed, err + } + + // use that to get the miner state + mas, err := miner.Load(adt.WrapStore(ctx, cstWithGas), act) + if err != nil { + return address.Undef, gasUsed, err + } + + info, err := mas.Info() + if err != nil { + return address.Undef, gasUsed, err + } + + stateTree, err := state.LoadStateTree(cstWithoutGas, x.base) + if err != nil { + return address.Undef, gasUsed, err + } + + raddr, err := ResolveToKeyAddr(stateTree, cstWithGas, info.Worker) + if err != nil { + return address.Undef, gasUsed, err + } + + return raddr, gasUsed, nil } type FVM struct { @@ -35,7 +230,7 @@ type FVM struct { func NewFVM(ctx context.Context, opts *VMOpts) (*FVM, error) { fvm, err := ffi.CreateFVM(0, - &FvmExtern{Rand: opts.Rand, Blockstore: opts.Bstore}, + &FvmExtern{Rand: opts.Rand, Blockstore: opts.Bstore, lbState: opts.LookbackState, base: opts.StateBase, epoch: opts.Epoch}, opts.Epoch, opts.BaseFee, opts.FilVested, opts.NetworkVersion, opts.StateBase, ) if err != nil { diff --git a/extern/filecoin-ffi b/extern/filecoin-ffi index d65d3770c90..9ff2301105f 160000 --- a/extern/filecoin-ffi +++ b/extern/filecoin-ffi @@ -1 +1 @@ -Subproject commit d65d3770c90ebdb8b3282f11fdf10a84c3ef0355 +Subproject commit 9ff2301105fcf25101f1fcda52e6417f3e2ca60b From 5be125ad1afe01ef6f3eff58eca9091623571bf5 Mon Sep 17 00:00:00 2001 From: Aayush Date: Wed, 16 Feb 2022 23:21:06 -0500 Subject: [PATCH 2/2] address review feedback --- chain/vm/fvm.go | 68 +++++++++++++++++++------------------------------ 1 file changed, 26 insertions(+), 42 deletions(-) diff --git a/chain/vm/fvm.go b/chain/vm/fvm.go index f98c043dd69..962b27be6c3 100644 --- a/chain/vm/fvm.go +++ b/chain/vm/fvm.go @@ -4,6 +4,8 @@ import ( "bytes" "context" + "github.com/filecoin-project/go-state-types/big" + "github.com/filecoin-project/lotus/chain/state" cbor "github.com/ipfs/go-ipld-cbor" @@ -36,13 +38,12 @@ type FvmExtern struct { base cid.Cid } -// Similar to the one in syscalls.go used by the Lotus VM, except it never errors +// VerifyConsensusFault is similar to the one in syscalls.go used by the Lotus VM, except it never errors // Errors are logged and "no fault" is returned, which is functionally what go-actors does anyway -func (x *FvmExtern) VerifyConsensusFault(ctx context.Context, a, b, extra []byte) *ffi_cgo.ConsensusFaultWithGas { - ret := &ffi_cgo.ConsensusFaultWithGas{ - // TODO: is this gonna be a problem on the receiving end? should we return address.NewIDAddress(0) instead? - Target: address.Undef, - Type: ffi_cgo.ConsensusFaultNone, +func (x *FvmExtern) VerifyConsensusFault(ctx context.Context, a, b, extra []byte) (*ffi_cgo.ConsensusFault, int64) { + totalGas := int64(0) + ret := &ffi_cgo.ConsensusFault{ + Type: ffi_cgo.ConsensusFaultNone, } // Note that block syntax is not validated. Any validly signed block will be accepted pursuant to the below conditions. @@ -56,42 +57,31 @@ func (x *FvmExtern) VerifyConsensusFault(ctx context.Context, a, b, extra []byte var blockA, blockB types.BlockHeader if decodeErr := blockA.UnmarshalCBOR(bytes.NewReader(a)); decodeErr != nil { log.Info("invalid consensus fault: cannot decode first block header: %w", decodeErr) - return ret + return ret, totalGas } if decodeErr := blockB.UnmarshalCBOR(bytes.NewReader(b)); decodeErr != nil { log.Info("invalid consensus fault: cannot decode second block header: %w", decodeErr) - return ret + return ret, totalGas } - // Commented out from the Lotus VM code: FvmExtern only supports v14 and onwards - // workaround chain halt - //if build.IsNearUpgrade(blockA.Height, build.UpgradeOrangeHeight) { - // return nil, xerrors.Errorf("consensus reporting disabled around Upgrade Orange") - //} - //if build.IsNearUpgrade(blockB.Height, build.UpgradeOrangeHeight) { - // return nil, xerrors.Errorf("consensus reporting disabled around Upgrade Orange") - //} - // are blocks the same? if blockA.Cid().Equals(blockB.Cid()) { log.Info("invalid consensus fault: submitted blocks are the same") - return ret + return ret, totalGas } // (1) check conditions necessary to any consensus fault // were blocks mined by same miner? if blockA.Miner != blockB.Miner { log.Info("invalid consensus fault: blocks not mined by the same miner") - return ret + return ret, totalGas } - ret.Target = blockA.Miner - // block a must be earlier or equal to block b, epoch wise (ie at least as early in the chain). if blockB.Height < blockA.Height { log.Info("invalid consensus fault: first block must not be of higher height than second") - return ret + return ret, totalGas } ret.Epoch = blockB.Height @@ -123,9 +113,7 @@ func (x *FvmExtern) VerifyConsensusFault(ctx context.Context, a, b, extra []byte if len(extra) > 0 { if decodeErr := blockC.UnmarshalCBOR(bytes.NewReader(extra)); decodeErr != nil { log.Info("invalid consensus fault: cannot decode extra: %w", decodeErr) - // just to match Lotus VM consensus, zero out any already-set faults - faultType = ffi_cgo.ConsensusFaultNone - return ret + return ret, totalGas } if types.CidArrsEqual(blockA.Parents, blockC.Parents) && blockA.Height == blockC.Height && @@ -137,7 +125,7 @@ func (x *FvmExtern) VerifyConsensusFault(ctx context.Context, a, b, extra []byte // (3) return if no consensus fault by now if faultType == ffi_cgo.ConsensusFaultNone { log.Info("invalid consensus fault: no fault detected") - return ret + return ret, totalGas } // else @@ -146,23 +134,24 @@ func (x *FvmExtern) VerifyConsensusFault(ctx context.Context, a, b, extra []byte // check blocks are properly signed by their respective miner // note we do not need to check extra's: it is a parent to block b // which itself is signed, so it was willingly included by the miner - gasUsed, sigErr := x.VerifyBlockSig(ctx, &blockA) - ret.GasUsed += gasUsed + gasA, sigErr := x.VerifyBlockSig(ctx, &blockA) + totalGas += gasA if sigErr != nil { log.Info("invalid consensus fault: cannot verify first block sig: %w", sigErr) - return ret + return ret, totalGas } - gasUsed, sigErr = x.VerifyBlockSig(ctx, &blockB) - ret.GasUsed += gasUsed + gas2, sigErr := x.VerifyBlockSig(ctx, &blockB) + totalGas += gas2 if sigErr != nil { log.Info("invalid consensus fault: cannot verify second block sig: %w", sigErr) - return ret + return ret, totalGas } ret.Type = faultType - - return ret + ret.Target = blockA.Miner + + return ret, totalGas } func (x *FvmExtern) VerifyBlockSig(ctx context.Context, blk *types.BlockHeader) (int64, error) { @@ -175,11 +164,6 @@ func (x *FvmExtern) VerifyBlockSig(ctx context.Context, blk *types.BlockHeader) } func (x *FvmExtern) workerKeyAtLookback(ctx context.Context, minerId address.Address, height abi.ChainEpoch) (address.Address, int64, error) { - // Commented out from the Lotus VM code: FvmExtern only supports v14 and onwards - //if x.networkVersion >= network.Version7 && height < x.epoch-policy.ChainFinality { - // return address.Undef, xerrors.Errorf("cannot get worker key (currEpoch %d, height %d)", ss.epoch, height) - //} - gasUsed := int64(0) gasAdder := func(gc GasCharge) { // technically not overflow safe, but that's fine @@ -261,11 +245,11 @@ func (vm *FVM) ApplyMessage(ctx context.Context, cmsg types.ChainMsg) (*ApplyRet }, GasCosts: &GasOutputs{ // TODO: do the other optional fields eventually - BaseFeeBurn: abi.TokenAmount{}, - OverEstimationBurn: abi.TokenAmount{}, + BaseFeeBurn: big.Zero(), + OverEstimationBurn: big.Zero(), MinerPenalty: ret.MinerPenalty, MinerTip: ret.MinerTip, - Refund: abi.TokenAmount{}, + Refund: big.Zero(), GasRefund: 0, GasBurned: 0, },