diff --git a/chain/vm/fvm.go b/chain/vm/fvm.go index 1e394a20258..962b27be6c3 100644 --- a/chain/vm/fvm.go +++ b/chain/vm/fvm.go @@ -1,10 +1,18 @@ package vm 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" + + "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 +21,191 @@ 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 +} + +// 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.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. + // 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, totalGas + } + + if decodeErr := blockB.UnmarshalCBOR(bytes.NewReader(b)); decodeErr != nil { + log.Info("invalid consensus fault: cannot decode second block header: %w", decodeErr) + return ret, totalGas + } + + // are blocks the same? + if blockA.Cid().Equals(blockB.Cid()) { + log.Info("invalid consensus fault: submitted blocks are the same") + 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, totalGas + } + + // 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, totalGas + } + + 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) + return ret, totalGas + } + + 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, totalGas + } + + // 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 + 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, totalGas + } + + 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, totalGas + } + + ret.Type = faultType + ret.Target = blockA.Miner + + return ret, totalGas } -func (x *FvmExtern) VerifyConsensusFault(ctx context.Context, h1, h2, extra []byte) (*ffi_cgo.ConsensusFault, error) { - // TODO - panic("unimplemented") +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) workerKeyAtLookback(ctx context.Context, minerId address.Address, height abi.ChainEpoch) (address.Address, int64, error) { + 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 +214,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 { @@ -66,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, }, 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